Skip to content

Rebuild v2: command bar, bulk actions, dialog-based duplicate and native create-issue fields - #65

Open
fathiraz wants to merge 109 commits into
mainfrom
rebuild/v2
Open

fathiraz wants to merge 109 commits into
mainfrom
rebuild/v2

Conversation

@fathiraz

@fathiraz fathiraz commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Full rewrite of the extension on top of the WXT + Primer React v38 stack:

  • Selection bar becomes a command palette (⌘K) covering every GitHub native field, relationship field (parent/blocked by/blocking), and project field, with option colors carried through.
  • Bulk verbs: field update, rename, reorder, mark, and deep duplicate — all paced through a single AsyncQueuer write queue (concurrency 1) with a live progress toast, retry, and dismiss.
  • Deep duplicate moved out of the palette into a Primer Dialog (one screen instead of a two-step wizard); everything carries over by default, with per-section toggles and an in-place title edit.
  • Create issue no longer replicates GitHub's composer — it injects a "Project fields" section directly into GitHub's own create-issue dialog, resolves the created issue's project item id via a dedicated query, and writes staged fields through the same queue. Handles "Create more", prefill-from-view group reflection, and reports rather than silently drops a field write when the new row never appears in the current view.
  • Row hovercard on the gutter cell showing live field values.
  • Background service worker owns the PAT and every GraphQL fetch; content script never calls GitHub directly.

Test plan

  • bun run test — 28 files / 190 tests green
  • bun run typecheck — clean
  • bun run lint — 0 errors (2 pre-existing warnings, unrelated files)
  • bun run build:chrome / build:firefox / build:edge
  • Manual /ego-browser QA against a live GitHub Projects board: selection, palette, bulk update, queue toast, deep duplicate dialog (open/edit/close/escape), create-issue field injection (single create, Create more, prefilled group note, fixed-height scroll, the "issue not in this view" failure path), row hovercard

Summary by cubic

Rebuilds the extension on WXT and Primer React v38 and switches the package manager from pnpm to bun. The selection bar becomes a ⌘K command palette covering GitHub native, relationship, and project fields; deep duplicate is now a single-screen Primer dialog; and create-issue injects a Project fields section into GitHub's own dialog instead of replicating its composer.

New Features

  • All bulk verbs (field update, rename, reorder, mark, deep duplicate) flow through one write queue with a live progress toast, retry, and dismiss.
  • The background service worker owns the PAT and every GraphQL fetch; the content script never calls GitHub directly.
  • Row hovercard on the gutter cell shows live field values.
  • A create-issue field write that can't land because the new row never appears in the current view is reported as a failure rather than dropped silently.

Migration

  • Developers now use bun: bun install, bun run test|typecheck|build, and per-browser zips via bun run zip:chrome etc.
  • Version bumps to 2.0.0; pnpm-lock.yaml and pnpm-workspace.yaml are replaced by bun.lock.

Written for commit f2e1252. Summary will update on new commits.

Review in cubic

Verified against a live project table (users/fathiraz/projects/1): the
Title cell is [role="rowheader"], a sibling of the [role="gridcell"]
drag-handle cell, not gridcells[1] as hypothesised. Mounts a 13x13
checkbox into the rowheader's title-cell wrapper instead of a manufactured
cell, avoiding the table's virtualised column-width CSS variables entirely.
Chromium's content-script isolated world exposes customElements as a
configurable getter-only accessor with value null. Plain assignment
silently no-ops there, so the crash from primer/live-region-element's
customElements.get() call at import time persisted. Object.defineProperty
actually replaces the accessor.
Rewrites the stale AGENTS.md sections (Primer v37/sx, shadow-DOM
CSUI) to describe the actual v2 architecture, and migrates ci.yml
and release.yml from pnpm to bun. ci.yml now also gates bun test.
coverage.yml is left on pnpm/vitest for now — bun test has no
cobertura reporter, which the coverage badge pipeline depends on;
that needs its own follow-up decision.
_tag was read in exactly one place (rehydrate's own switch) and only
GithubRateLimitError was ever discriminated at runtime. One class
carrying a _tag field makes serialize a destructure and drops
rehydrate entirely; classifyHttpError keeps every branch.

Also drops PatErrorType/PatError (zero references in src/ or test/).
validateToken re-implemented gql() line for line: same endpoint,
headers, 30s timeout, network catch, and !res.ok classification.
githubFetch is that shared half, returning the raw Response so
validateToken can still read x-oauth-scopes. gql's dead token?
param drops with it — githubFetch now owns token resolution.

validateToken also picks up real retry-after/x-ratelimit-remaining
classification instead of hardcoded zeros, and the same
GitHub-Feature-Request header the main path already sends.
- MARK ('data-rgp') was write-only: set on every row, read nowhere
  in JS or CSS. The mount guard already checks slot presence instead.
- pnpm-workspace.yaml is an orphan; the rewrite dropped pnpm-lock.yaml
  and no workflow or script references pnpm.
- queueState's export had one consumer, useQueueState, ten lines
  below it in the same file.
eslint.config.mjs and .prettierrc were tracked but imported packages
never installed on this branch, and there was no lint/format script.
Pulls the working eslint/typescript-eslint/eslint-plugin-react-hooks/
eslint-config-prettier/prettier versions and scripts from main.

Also reformats write-queue.ts's one line-width violation, caught by
running prettier for the first time on this branch.
The Errors paragraph described the 7-class ADT and switch-on-_tag/
instanceof narrowing that no longer exist post-collapse. Also notes
the githubFetch seam alongside the existing 'where to add new code'
guidance.
itemIdOf() read GitHub's issue hovercard tag, which updateProjectV2ItemFieldValue
rejects — it needs the item's own opaque node id. Query items(first: 100) on the
project fields fetch, map issue number to item id, and resolve through it before
enqueueing writes.
Add getTokenStatus/clearToken messages backed by tokenMetaStorage so the
popup can show a Connected state without re-storing scopes on every check.
Tighten the mutation queue's pace to 500ms between writes.
…ookup

itemIdOf(row) returns the row's issue number, not the GraphQL item id
fetchItemDetails needs. Look it up via the same project query
selection-bar.tsx already caches under ['fields', login, number].
…loses

The announcement-node observer never fired because GitHub tears the whole
dialog down on a successful create instead of mutating that node in place.
Move the post-create hook into the existing dialog-presence observer, reading
staged chips through a ref so they survive the parent's own unmount timing.
Clearing it unconditionally in sync() let a fast second "Create more" issue's click evidence be discarded while the reentrancy guard was still blocking on a prior in-flight apply, silently dropping its staged fields.
@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / Security Evidence

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

Security scanner evidence required (action_required)

Detected 1 security-sensitive predictive risk signal(s) without scanner evidence.

Mode: enforce

Findings:

  • Security-sensitive changes may ship without scanner evidence: The PR touches billing, secrets, auth, webhooks, agent, or CI-sensitive surfaces without adding obvious security scanner, code scanning, or security-focused validation evidence. (5 security-sensitive paths changed; 0 security scanner or security-focused validation artifacts changed)

Touched security-sensitive paths:

  • src/ui/token/account-row.tsx
  • src/ui/token/disconnect-confirm.tsx

Expected evidence:

  • Security scanner, code scanning, secret scanning, dependency/security review, or focused security regression output.
  • SARIF/code-scanning upload or equivalent pass/fail gate for the changed surface.

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / PR Risk Taxonomy

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

PR taxonomy review recommended (neutral)

Detected 7 PR taxonomy bucket(s): Security Evidence, Harness Drift, Install Manifest Integrity, CI/CD Recommendation, Cost/Token Risk, PR Review/Salvage Evidence, Agent Config Review.

Scanned 266 changed file(s).

Roadmap taxonomy buckets:

Security Evidence

Security-sensitive changes should carry explicit scanner, code-scanning, or focused regression evidence.

Signals:

  • Security-sensitive changes may ship without scanner evidence
  • 9 security-sensitive path(s) changed

Paths:

  • .github/workflows/ci.yml
  • .github/workflows/coverage.yml
  • .github/workflows/release.yml
  • src/features/token-setup.tsx
  • src/ui/token/account-row.tsx
  • src/ui/token/disconnect-confirm.tsx
  • src/ui/token/scope-chips.tsx
  • src/ui/token/status-callout.tsx

Harness Drift

Harness-facing changes can drift across Claude Code, Codex, OpenCode, and shared adapter surfaces.

Signals:

  • 1 harness-facing path(s) changed

Paths:

  • AGENTS.md

Install Manifest Integrity

Install manifests, plugin metadata, and shipped skills should stay synchronized with user-facing setup guidance.

Signals:

  • 2 install or manifest path(s) changed

Paths:

  • package.json
  • pnpm-lock.yaml

CI/CD Recommendation

CI, dependency, coverage, and contract signals should be routed into follow-up checks or verification work.

Signals:

  • Runtime config changes may ship without example or template updates
  • Async job or webhook changes may ship without reliability coverage
  • User-facing UI changes may ship without browser coverage
  • CI workflow changes may ship without failure-mode evidence
  • 81 CI or workflow path(s) changed

Paths:

  • .github/workflows/ci.yml
  • .github/workflows/coverage.yml
  • .github/workflows/release.yml
  • package.json
  • pnpm-lock.yaml
  • src/background/__tests__/bulk-position.test.ts
  • src/background/__tests__/bulk-rename.test.ts
  • src/background/__tests__/bulk-state.test.ts

Cost/Token Risk

AI routing, usage, and token-budget changes should include budget or usage-limit evidence.

Signals:

  • Cost or token-risk changes may ship without budget evidence
  • 6 cost/token path(s) changed

Paths:

  • src/features/token-setup.tsx
  • src/ui/token/account-row.tsx
  • src/ui/token/disconnect-confirm.tsx
  • src/ui/token/scope-chips.tsx
  • src/ui/token/status-callout.tsx
  • src/ui/token/style.css

PR Review/Salvage Evidence

PR review, stale-closure, and triage automation should carry stale-closure, reviewer-thread, and reopen-flow evidence so useful closed work can still be salvaged.

Signals:

  • PR review or stale-closure automation may ship without salvage evidence
  • 1 PR review/salvage path(s) changed

Paths:

  • src/lib/github.ts

Agent Config Review

Agent, command, skill, MCP, and local instruction changes should be reviewed as executable agent configuration.

Signals:

  • 1 agent-config path(s) changed

Paths:

  • AGENTS.md

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / Reference Set Readiness

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

Reference set readiness gaps detected (neutral)

Reference evidence present for 1/7 areas (14%) across 266 changed file(s).

This check is based on files changed in this PR. Repository-level readiness is still reported by /ecc-tools analyze comments and generated manifests.

Area Status Evidence / Next Step
Deep analyzer corpus Missing Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions.
RAG/evaluator comparison Present src/lib/storage.test.ts
PR salvage/review corpus Missing Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation.
Discussion triage corpus Missing Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications.
Harness compatibility Missing Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces.
Security evidence Missing Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs.
CI failure-mode evidence Missing Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes.

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / Hosted Promotion Readiness

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

Hosted promotion evidence recommended (neutral)

Evaluator corpus matched 1 hosted promotion evidence gap across 266 changed file(s).

This check compares PR file changes against the evaluator/RAG promotion corpus in src/analyzers/fixtures/evaluator-rag-corpus.ts.
Hosted output scoring inspected 0 completed cached hosted job results.

Scenario Status Changed paths Expected evidence Hosted output score
stale-pr-salvage-maintainer-branch Missing evidence src/lib/github.ts tests/fixtures/stale-salvage/reopen-flow.json 0 - no matching completed hosted output

Retrieval and model promotion plan

stale-pr-salvage-maintainer-branch

Top retrieval candidates:

  • 80 expected evidence tests/fixtures/stale-salvage/reopen-flow.json - expected evidence target from the evaluator/RAG corpus
  • 40 changed path src/lib/github.ts - changed source path that triggered the promotion scenario

Model prompt seed: Decide whether stale-pr-salvage-maintainer-branch has enough hosted evidence for promotion. Expected issue keys: pr-review-salvage-gap. Expected evidence buckets: PR Review/Salvage Evidence. Compare the changed paths against the ranked retrieval candidates. Do not promote from changed paths alone.

Model-backed promotion judging contract

Scenario Gate Request Blockers
stale-pr-salvage-maintainer-branch Blocked Not queued free tier is not entitled to hosted model judging; no hosted retrieval evidence candidate

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / PR Config Audit

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

No changed-config issues detected (success)

Scanned 4 config file(s) present at this commit across 4 changed config path(s) and found no issues in the supported security rules.

Changed config files:

  • .github/workflows/ci.yml
  • .github/workflows/coverage.yml
  • .github/workflows/release.yml
  • AGENTS.md

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@ecc-tools

ecc-tools Bot commented Sep 15, 2026

Copy link
Copy Markdown

ECC Tools / PR Harness Audit

Commit: f2e12528936ce6378c9c9860e7ed827a5f649d0e

Harness warnings detected (neutral)

Scanned 4 changed config file(s) and found 1 harness issue(s).

  • [medium] ECC manifest missing (AGENTS.md)

Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Left a non-blocking comment and did not approve; this rebuild is above the medium approval threshold (broad extension rewrite, PAT/auth path, CI/release workflows, and large test deletions). Cursor Bugbot was not present after the first check poll, and human review is needed. Reviewers were not assigned because no eligible non-author collaborator is available.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor auto review

Found 6 actionable issue(s) on changed lines.

Several high-confidence correctness bugs in queue cancel/retry, failure attribution, secondary rate-limit classification, PR row DOM selection, and duplicate create chaining.

Generated automatically when this PR was submitted using Cursor CLI with --model auto.

}
return null
export function itemIdOf(row: HTMLElement): string | null {
const href = row.querySelector<HTMLAnchorElement>('a[href*="/issues/"]')?.getAttribute('href')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemIdOf only matches a[href*="/issues/"], so PR rows (/pull/N) never get selection ids. ProjectItem and issueStates already model PullRequest (including .octicon-git-pull-request), and those rows can also match isGroupHeaderRow because itemIdOf returns null. Match /issues/ and /pull/ (and the same in issueRefOf / rowTitles).

Comment thread src/lib/write-queue.ts
{
mutationKey: ['writeTask'],
mutationFn: (t: QueuedTask) =>
request(t.query, typeof t.variables === 'function' ? t.variables() : t.variables),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mutationFn always calls request() without checking task.gen === generation. After cancelQueue() during a Retry-After pause, TanStack retries still write to GitHub; only UI accounting is skipped. Gate every attempt (or abort the mutation) on the current generation before sending.

Comment thread src/lib/write-queue.ts
* request to re-send. */
export function reportFailure(run: QueueRun, label: string, message: string): void {
clearDismiss()
if (!state.running) set({ ...IDLE, ...run })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When state.running is true, reportFailure still settles into the live run. create-issue-dom.ts calls this after enqueueWrites returns false, so a create-issue miss is charged to an unrelated bulk/duplicate card (wrong totals and Retry Failed rows). If a different run is active, no-op or open a separate failure surface instead of mutating the current run.

Comment thread src/lib/errors.ts
@@ -121,28 +60,12 @@ export function classifyHttpError(args: {
// message for permission failures.
if (status === 403) {
return args.rateLimitRemaining === 0
? new GithubRateLimitError(args)
: new GithubClientError({ status, message })
? new GithubError({ ...args, _tag: 'GithubRateLimitError' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

403 is rate-limited only when rateLimitRemaining === 0. GitHub secondary/abuse 403s often send Retry-After while remaining is null or non-zero; those become GithubClientError and skip retry. Treat 403 as GithubRateLimitError when retryAfter > 0 or rateLimitRemaining === 0.

Comment thread src/lib/write-queue.ts
concurrency: 1,
wait: 500,
started: true,
throwOnError: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwOnError: false lets the queue continue after a failed task. duplicateTasks then still runs ADD_TO_PROJECT / field / relationship mutations with empty issueId/itemId, causing extra failing GraphQL writes. On error, stop or dequeue the rest of that chain (or skip thunks when ids were never set).

verb: 'Duplicating',
})
if (!started) {
setBusy(true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setBusy(true) on a refused enqueue never resets. With "Create more", a later successful Duplicate still leaves BusyFlash visible. Clear busy on successful enqueueWrites, when the active run finishes, or when the dialog closes.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 issues found across 266 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/lib/github.ts">

<violation number="1" location="src/lib/github.ts:311">
P1: Draft issue rows pass the null check but have none of the Issue/PullRequest fields selected, so `fetchProject` adds an item containing undefined identity and repository values. Filter content by `Issue`/`PullRequest` before mapping it, or explicitly support draft issues.</violation>

<violation number="2" location="src/lib/github.ts:314">
P1: When a project contains the same issue number from two repositories, selecting one row selects both and bulk writes target only one. Key selections by a repository-qualified or content ID instead of the issue number.</violation>
</file>

<file name="src/ui/duplicate-dialog.tsx">

<violation number="1" location="src/ui/duplicate-dialog.tsx:101">
P2: When `fetchItemDetails` fails, React Query leaves `isPending` false and `data` undefined, so this guard renders the spinner forever with no error or retry action. Render the query error state separately so users can close or retry the duplicate.</violation>

<violation number="2" location="src/ui/duplicate-dialog.tsx:101">
P2: When details loading fails or the selected item is a pull request, this dialog stays on its loading spinner indefinitely. Handle the query error with an actionable state and gate or explicitly support pull requests before opening the duplicate flow.</violation>

<violation number="3" location="src/ui/duplicate-dialog.tsx:134">
P1: When `Create more` is checked, `fire()` clears the selection before retaining the dialog. The dialog then receives no item and stays on its loading screen. Keep the selection while `more` is true, and clear it only when closing after the final duplicate.</violation>
</file>

<file name="src/ui/create-issue-fields.tsx">

<violation number="1" location="src/ui/create-issue-fields.tsx:54">
P1: When a second Create more click happens while the first field apply is in flight, this guard drops the observer pass and `finally` schedules no retry. Queue the pending create evidence/row and process it after the current apply completes instead of leaving the second issue's fields unapplied.</violation>

<violation number="2" location="src/ui/create-issue-fields.tsx:123">
P1: When GitHub replaces the injected slot during a dialog re-render, `FieldsSection` remounts and resets `chips` to `[]`, so staged project fields disappear before Create. Keep the chips in the parent or restore them when recreating the portal target.</violation>
</file>

<file name="src/lib/project-table-dom.ts">

<violation number="1" location="src/lib/project-table-dom.ts:6">
P1: Pull requests are silently excluded from every row feature because `itemIdOf` only recognizes `/issues/` links. Match both `/issues/` and `/pull/` paths so project rows containing PRs receive the same behavior as issue rows.</violation>

<violation number="2" location="src/lib/project-table-dom.ts:7">
P1: This identity extractor drops pull-request rows and conflates equal issue numbers across repositories. Parse both issue and pull links, then use an owner/repository/number key consistently instead of a bare issue number.</violation>
</file>

<file name="src/ui/field-editor.tsx">

<violation number="1" location="src/ui/field-editor.tsx:140">
P1: When a user switches directly from one open chip/field to another, `FieldEditor` keeps the previous draft, query, and mode because these `useState` initializers do not react to `initial` changes. Reset editor state when the field/value identity changes, or key each mounted editor by its field/chip.</violation>

<violation number="2" location="src/ui/field-editor.tsx:233">
P1: When this editor is used inside GitHub’s create-issue form, pressing Enter commits the field and also triggers the form’s implicit submit. Call `e.preventDefault()` before `onCommit` so Enter only applies the field.</violation>
</file>

<file name="test/gql.test.ts">

<violation number="1" location="test/gql.test.ts:2">
P1: Because this static import creates Ky before the tests replace `globalThis.fetch`, these fixtures never intercept `gql` or `githubFetch`; the tests can make real GitHub requests and fail. Mock `fetch` before importing this module, or configure Ky with a call-time fetch delegate.</violation>
</file>

<file name="src/lib/create-issue-dom.ts">

<violation number="1" location="src/lib/create-issue-dom.ts:45">
P1: In a multi-repository project, an existing `#121` from one repository makes `newRowRef()` reject a newly created `#121` from another. Key `known` by owner, repository, and number instead of number alone.</violation>

<violation number="2" location="src/lib/create-issue-dom.ts:141">
P1: After a Create-more click with no staged fields, the next issue's field edits can consume the stale `createClicked` flag and apply them to the earlier row. Reset click evidence when that click is consumed, even when no field write is needed.</violation>

<violation number="3" location="src/lib/create-issue-dom.ts:227">
P1: If another write run starts while this create flow awaits the item lookup, `enqueueWrites()` returns false. `reportFailure()` then pollutes that unrelated run with an unretryable create failure; reserve queue admission or retain the create work until the queue is available.</violation>
</file>

<file name="src/entries/content/index.tsx">

<violation number="1" location="src/entries/content/index.tsx:69">
P1: When a group expands or the table virtualizes new rows, these checkboxes keep the IDs captured during their first mount. Update the existing checkbox's ID set or remount it so group and “all loaded items” selection covers current rows.</violation>

<violation number="2" location="src/entries/content/index.tsx:82">
P2: When GitHub loads or virtualizes additional rows after the header mounts, the header checkbox keeps the initial `loadedIds()` snapshot and select-all omits those rows. Derive the IDs at click time or update the mounted checkbox whenever the loaded rows change.</violation>

<violation number="3" location="src/entries/content/index.tsx:115">
P1: Because this root is registered without the name used by Primer's default portal, the overlays still render in GitHub's existing portal instead of the new body-level root. Register it under the default portal name or pass the same named `portalContainerName` to every overlay.</violation>

<violation number="4" location="src/entries/content/index.tsx:119">
P1: After client-side navigation to another project, the mounted tree keeps the first project's fields and `projectId`, so create-issue and bulk writes can target the previous project. Detect project URL changes and reset or remount the project-scoped tree and selection.</violation>
</file>

<file name="src/lib/reorder.ts">

<violation number="1" location="src/lib/reorder.ts:22">
P1: When a project contains issues from different repositories with the same number, `reorderOps` treats every matching row as selected and moves both. Key selection by a repository-qualified identity or project item ID instead of `number`.</violation>
</file>

<file name="src/ui/mark-step.tsx">

<violation number="1" location="src/ui/mark-step.tsx:45">
P1: When a selection contains a pull request, Mark's close or reopen task sends its ID to the issue-only mutation. Choose the pull-request mutation from `item.typename` before enqueueing each task.</violation>

<violation number="2" location="src/ui/mark-step.tsx:127">
P1: When a user chooses any lock reason other than the default, `onChange` stores the event object instead of its value, so the queued lock mutation sends an invalid `$reason` and fails. Read `event.target.value` before casting it to `LockReason`.</violation>
</file>

<file name="src/lib/item-details.ts">

<violation number="1" location="src/lib/item-details.ts:9">
P1: When a pull request is hovered or selected for Deep duplicate, `fetchItemDetails` returns no content because it queries only `Issue`. Add a PullRequest fragment and normalize it, or disable the unsupported action and show a hovercard error.</violation>
</file>

<file name="src/lib/write-queue.ts">

<violation number="1" location="src/lib/write-queue.ts:132">
P1: When the user cancels during a rate-limit backoff, the active mutation still retries because its retry predicate ignores `generation` and its mutation function always calls `request`. Stop retries when the task generation is stale.</violation>

<violation number="2" location="src/lib/write-queue.ts:161">
P2: When create-issue lookup finishes during another run, `reportFailure` settles that active run, mislabels the card and mixes the failure into Retry Failed. Keep failures run-scoped and defer or separately surface them.</violation>
</file>

<file name="src/ui/selection-bar.tsx">

<violation number="1" location="src/ui/selection-bar.tsx:64">
P2: While the duplicate dialog is open, global bar shortcuts can replace it and lose the user's title or toggle edits. Suppress these shortcuts whenever the extension dialog owns focus.</violation>

<violation number="2" location="src/ui/selection-bar.tsx:94">
P2: On the first ⌘K with no selection, `focusInput()` runs before the bar exists, so the opened palette cannot receive typing, arrow-key navigation, or Enter. Focus after the root render or keep a focusable bar mounted while closed.</violation>

<violation number="3" location="src/ui/selection-bar.tsx:99">
P2: With no selection, `⌘K` cannot open the palette because `SelectionBar` returns before installing its only `⌘K` handler. Mount a global `⌘K` launcher or keep the launcher mounted independently of selection.</violation>

<violation number="4" location="src/ui/selection-bar.tsx:121">
P2: When Mark, Rename, Reorder, or custom position is open, the bar input remains focused but `rowsFor` exposes no rows. Arrow keys and Enter therefore cannot operate the visible palette actions; add matching row/commit handling or keep focus in the active step.</violation>
</file>

<file name="src/ui/palette.tsx">

<violation number="1" location="src/ui/palette.tsx:303">
P2: When the current view has no inline Add item row, `focusAddItem()` returns false but this handler still closes the palette, leaving New issue with no usable flow. Close the palette only after `focusAddItem()` succeeds.</violation>
</file>

<file name="src/lib/messages.ts">

<violation number="1" location="src/lib/messages.ts:27">
P2: When the MV3 service worker has just been suspended, the raw sender can reject before the listener is ready, so the first request after idle fails. Restore the reconnect retry around `sendMessage` (or an equivalent retry), because the query retry policy does not retry plain messaging errors.</violation>
</file>

<file name="src/lib/chords.ts">

<violation number="1" location="src/lib/chords.ts:37">
P2: When a native GitHub or duplicate-dialog text field has focus, `chordFor` still treats `⌘⇧E/R/J` as extension commands because the modifier branch bypasses `inInput`. Distinguish the bar’s own input from other editing targets before allowing these chords, so typing in GitHub dialogs cannot unexpectedly open or reset bulk actions.</violation>
</file>

<file name="src/lib/gql.ts">

<violation number="1" location="src/lib/gql.ts:38">
P2: When a 403 response has a malformed `X-RateLimit-Remaining` header, `numericHeader` classifies it as `GithubRateLimitError` and the query client retries a permission failure. Preserve invalid values as missing/nonzero so only an actual numeric zero enters the rate-limit path.

(Based on your team's feedback about distinguishing 403 rate limits from permission failures.)</violation>

<violation number="2" location="src/lib/gql.ts:104">
P2: When the response body is valid JSON `null`, optional chaining on `errors` does not protect the null `body`, so `decode` rejects instead of returning `GithubDecodeError`. Validate that the decoded value is a non-null object before reading `errors` or `data`.</violation>
</file>

<file name="src/ui/row-hovercard.tsx">

<violation number="1" location="src/ui/row-hovercard.tsx:105">
P2: When a project contains the same issue number in multiple repositories, this lookup can load the first repository's hovercard for the row under the pointer. Match the row's repository and number, or resolve the item ID directly from the row's full content reference.</violation>

<violation number="2" location="src/ui/row-hovercard.tsx:119">
P2: When the item-details result is cached, the first layout pass measures no card and the effect never reruns after mounting it. Measure after the card mounts, otherwise bottom-row hovercards can render off-screen instead of using the `above` placement.</violation>
</file>

<file name="src/lib/field-cache.ts">

<violation number="1" location="src/lib/field-cache.ts:5">
P2: After switching projects, the palette can use the previous project’s cached field IDs while the new query is pending or failed. Scope the persisted cache by project identity and only use a matching cache entry.</violation>
</file>

<file name="src/lib/duplicate-plan.ts">

<violation number="1" location="src/lib/duplicate-plan.ts:74">
P2: When issue creation or project attachment fails, the queue still runs dependent duplicate tasks with empty IDs. Stop the dependent chain after a prerequisite failure so the user gets one actionable failure instead of a cascade of invalid writes.</violation>
</file>

<file name="src/ui/queue-toast.tsx">

<violation number="1" location="src/ui/queue-toast.tsx:94">
P2: When a partial run contains a synthetic failure, such as a new issue that is not visible in the current view, this button still calls `retryFailed`. `retryFailed` drops those empty-query failures and clears the card, so clicking “Retry Failed” dismisses the only failure instead of retrying or preserving it. Hide this action when no failures are resendable, or update the retry flow to retain non-retryable failures.</violation>
</file>

<file name="src/lib/rename-rules.ts">

<violation number="1" location="src/lib/rename-rules.ts:99">
P2: When literal find mode is selected, replacement text containing `$&`, `$\`` or `$'` is rewritten by JavaScript instead of being inserted literally. Use a replacement callback in literal mode, while preserving substitution syntax only for regex mode.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Re-trigger cubic

Comment thread src/lib/github.ts
if (!node.content) continue
items.push({
itemId: node.id,
number: node.content.number,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a project contains the same issue number from two repositories, selecting one row selects both and bulk writes target only one. Key selections by a repository-qualified or content ID instead of the issue number.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/github.ts, line 314:

<comment>When a project contains the same issue number from two repositories, selecting one row selects both and bulk writes target only one. Key selections by a repository-qualified or content ID instead of the issue number.</comment>

<file context>
@@ -0,0 +1,336 @@
+      if (!node.content) continue
+      items.push({
+        itemId: node.id,
+        number: node.content.number,
+        contentId: node.content.id,
+        title: node.content.title,
</file context>

Comment on lines +134 to +140
selection.clear()
if (more) {
setChoices(defaultChoices(details))
setBodyOverride(null)
} else {
onClose()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When Create more is checked, fire() clears the selection before retaining the dialog. The dialog then receives no item and stays on its loading screen. Keep the selection while more is true, and clear it only when closing after the final duplicate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/duplicate-dialog.tsx, line 134:

<comment>When `Create more` is checked, `fire()` clears the selection before retaining the dialog. The dialog then receives no item and stays on its loading screen. Keep the selection while `more` is true, and clear it only when closing after the final duplicate.</comment>

<file context>
@@ -0,0 +1,232 @@
+      setBusy(true)
+      return
+    }
+    selection.clear()
+    if (more) {
+      setChoices(defaultChoices(details))
</file context>
Suggested change
selection.clear()
if (more) {
setChoices(defaultChoices(details))
setBodyOverride(null)
} else {
onClose()
}
if (more) {
setChoices(defaultChoices(details))
setBodyOverride(null)
} else {
selection.clear()
onClose()
}

* follow-up (Q9/Q11). */
function FieldsSection({ chipsRef }: SectionProps) {
const { fields } = useProjectFields()
const [chips, setChips] = useState<Chip[]>([])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When GitHub replaces the injected slot during a dialog re-render, FieldsSection remounts and resets chips to [], so staged project fields disappear before Create. Keep the chips in the parent or restore them when recreating the portal target.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/create-issue-fields.tsx, line 123:

<comment>When GitHub replaces the injected slot during a dialog re-render, `FieldsSection` remounts and resets `chips` to `[]`, so staged project fields disappear before Create. Keep the chips in the parent or restore them when recreating the portal target.</comment>

<file context>
@@ -0,0 +1,204 @@
+ *  follow-up (Q9/Q11). */
+function FieldsSection({ chipsRef }: SectionProps) {
+  const { fields } = useProjectFields()
+  const [chips, setChips] = useState<Chip[]>([])
+  const [open, setOpen] = useState<string | null>(null)
+  const [collapsed, setCollapsed] = useState(false)
</file context>

return null
export function itemIdOf(row: HTMLElement): string | null {
const href = row.querySelector<HTMLAnchorElement>('a[href*="/issues/"]')?.getAttribute('href')
return href?.match(/\/issues\/(\d+)/)?.[1] ?? null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This identity extractor drops pull-request rows and conflates equal issue numbers across repositories. Parse both issue and pull links, then use an owner/repository/number key consistently instead of a bare issue number.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/project-table-dom.ts, line 7:

<comment>This identity extractor drops pull-request rows and conflates equal issue numbers across repositories. Parse both issue and pull links, then use an owner/repository/number key consistently instead of a bare issue number.</comment>

<file context>
@@ -1,210 +1,176 @@
-  return null
+export function itemIdOf(row: HTMLElement): string | null {
+  const href = row.querySelector<HTMLAnchorElement>('a[href*="/issues/"]')?.getAttribute('href')
+  return href?.match(/\/issues\/(\d+)/)?.[1] ?? null
 }
 
</file context>

Comment thread src/ui/field-editor.tsx
export function FieldEditor(props: FieldEditorProps): ReactNode {
const { field, items, initial, onCommit, onCancel, bar } = props
const [query, setQuery] = useState('')
const [draft, setDraft] = useState(initial?.display ?? '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a user switches directly from one open chip/field to another, FieldEditor keeps the previous draft, query, and mode because these useState initializers do not react to initial changes. Reset editor state when the field/value identity changes, or key each mounted editor by its field/chip.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/field-editor.tsx, line 140:

<comment>When a user switches directly from one open chip/field to another, `FieldEditor` keeps the previous draft, query, and mode because these `useState` initializers do not react to `initial` changes. Reset editor state when the field/value identity changes, or key each mounted editor by its field/chip.</comment>

<file context>
@@ -0,0 +1,467 @@
+export function FieldEditor(props: FieldEditorProps): ReactNode {
+  const { field, items, initial, onCommit, onCancel, bar } = props
+  const [query, setQuery] = useState('')
+  const [draft, setDraft] = useState(initial?.display ?? '')
+  const [mode, setMode] = useState<'add' | 'remove'>(initial?.mode ?? 'add')
+  const bodyRef = useRef<HTMLTextAreaElement>(null)
</file context>

Comment thread src/ui/queue-toast.tsx
))}
</ul>
<div className="rgp-queue-actions">
<Button size="small" onClick={retryFailed}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a partial run contains a synthetic failure, such as a new issue that is not visible in the current view, this button still calls retryFailed. retryFailed drops those empty-query failures and clears the card, so clicking “Retry Failed” dismisses the only failure instead of retrying or preserving it. Hide this action when no failures are resendable, or update the retry flow to retain non-retryable failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/queue-toast.tsx, line 94:

<comment>When a partial run contains a synthetic failure, such as a new issue that is not visible in the current view, this button still calls `retryFailed`. `retryFailed` drops those empty-query failures and clears the card, so clicking “Retry Failed” dismisses the only failure instead of retrying or preserving it. Hide this action when no failures are resendable, or update the retry flow to retain non-retryable failures.</comment>

<file context>
@@ -0,0 +1,131 @@
+                ))}
+              </ul>
+              <div className="rgp-queue-actions">
+                <Button size="small" onClick={retryFailed}>
+                  Retry Failed
+                </Button>
</file context>

Comment thread src/lib/gql.ts
}
}

if (body.errors?.length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the response body is valid JSON null, optional chaining on errors does not protect the null body, so decode rejects instead of returning GithubDecodeError. Validate that the decoded value is a non-null object before reading errors or data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/gql.ts, line 104:

<comment>When the response body is valid JSON `null`, optional chaining on `errors` does not protect the null `body`, so `decode` rejects instead of returning `GithubDecodeError`. Validate that the decoded value is a non-null object before reading `errors` or `data`.</comment>

<file context>
@@ -0,0 +1,123 @@
+    }
+  }
+
+  if (body.errors?.length) {
+    return {
+      ok: false,
</file context>
Suggested change
if (body.errors?.length) {
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
return {
ok: false,
error: serialize(new GithubError({ _tag: 'GithubDecodeError', message: 'Bad GraphQL response' })),
}
}
if (body.errors?.length) {

Comment thread src/lib/rename-rules.ts
if (error === null) {
switch (rule.tab) {
case 'replace':
next = re ? item.title.replace(re, rule.replaceText) : item.title

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When literal find mode is selected, replacement text containing $&, $\`` or $'` is rewritten by JavaScript instead of being inserted literally. Use a replacement callback in literal mode, while preserving substitution syntax only for regex mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/rename-rules.ts, line 99:

<comment>When literal find mode is selected, replacement text containing `$&`, `$\`` or `$'` is rewritten by JavaScript instead of being inserted literally. Use a replacement callback in literal mode, while preserving substitution syntax only for regex mode.</comment>

<file context>
@@ -0,0 +1,119 @@
+    if (error === null) {
+      switch (rule.tab) {
+        case 'replace':
+          next = re ? item.title.replace(re, rule.replaceText) : item.title
+          break
+        case 'prefix':
</file context>

Comment thread src/ui/row-hovercard.tsx
useLayoutEffect(() => {
if (!anchor) return
const rect = anchor.getBoundingClientRect()
const height = cardRef.current?.offsetHeight ?? 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the item-details result is cached, the first layout pass measures no card and the effect never reruns after mounting it. Measure after the card mounts, otherwise bottom-row hovercards can render off-screen instead of using the above placement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/row-hovercard.tsx, line 119:

<comment>When the item-details result is cached, the first layout pass measures no card and the effect never reruns after mounting it. Measure after the card mounts, otherwise bottom-row hovercards can render off-screen instead of using the `above` placement.</comment>

<file context>
@@ -0,0 +1,200 @@
+  useLayoutEffect(() => {
+    if (!anchor) return
+    const rect = anchor.getBoundingClientRect()
+    const height = cardRef.current?.offsetHeight ?? 0
+    const flip = rect.bottom + height + 8 > window.innerHeight
+    setPos({
</file context>

Comment thread src/ui/selection-bar.tsx
// return-focus target — closing the dialog would then land focus back on
// this input, whose own onFocus reopens the palette right behind it.
if (next && next.kind !== 'duplicate' && !(next.kind === 'field' && isTyped(next.field)))
focusInput()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: On the first ⌘K with no selection, focusInput() runs before the bar exists, so the opened palette cannot receive typing, arrow-key navigation, or Enter. Focus after the root render or keep a focusable bar mounted while closed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ui/selection-bar.tsx, line 94:

<comment>On the first ⌘K with no selection, `focusInput()` runs before the bar exists, so the opened palette cannot receive typing, arrow-key navigation, or Enter. Focus after the root render or keep a focusable bar mounted while closed.</comment>

<file context>
@@ -0,0 +1,319 @@
+    // return-focus target — closing the dialog would then land focus back on
+    // this input, whose own onFocus reopens the palette right behind it.
+    if (next && next.kind !== 'duplicate' && !(next.kind === 'field' && isTyped(next.field)))
+      focusInput()
+  }
+
</file context>

Comment thread src/ui/selection-bar.tsx
}, [selected.size])

const items = allItems
const byNumber = useMemo(() => new Map(items.map((i) => [String(i.number), i])), [items])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review
Severity: MEDIUM

Selection and bulk writes key GitHub items by bare issue number. byNumber is a Map keyed on String(i.number), so a multi-repo project with two #N items keeps only one mapping (last write wins). Apply then mutates item.contentId / item.itemId for that winner, not necessarily the row the user checked. The same number-only lookup is used in mark, reorder, and hovercard.

Impact: A bulk field update, rename, close/lock, or hovercard fetch can target a different issue that shares the number, including an item in another repository on the same board, using the stored PAT.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit f2e1252. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant