Conversation
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].
…lection clear shortcut
…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.
…arm on every exit
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 / Security EvidenceCommit: Security scanner evidence required (action_required) Detected 1 security-sensitive predictive risk signal(s) without scanner evidence. Mode: enforce Findings:
Touched security-sensitive paths:
Expected evidence:
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 / PR Risk TaxonomyCommit: 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 EvidenceSecurity-sensitive changes should carry explicit scanner, code-scanning, or focused regression evidence. Signals:
Paths:
Harness DriftHarness-facing changes can drift across Claude Code, Codex, OpenCode, and shared adapter surfaces. Signals:
Paths:
Install Manifest IntegrityInstall manifests, plugin metadata, and shipped skills should stay synchronized with user-facing setup guidance. Signals:
Paths:
CI/CD RecommendationCI, dependency, coverage, and contract signals should be routed into follow-up checks or verification work. Signals:
Paths:
Cost/Token RiskAI routing, usage, and token-budget changes should include budget or usage-limit evidence. Signals:
Paths:
PR Review/Salvage EvidencePR 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:
Paths:
Agent Config ReviewAgent, command, skill, MCP, and local instruction changes should be reviewed as executable agent configuration. Signals:
Paths:
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 / Reference Set ReadinessCommit: 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
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 / Hosted Promotion ReadinessCommit: 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
Retrieval and model promotion planstale-pr-salvage-maintainer-branchTop retrieval candidates:
Model prompt seed: Decide whether Model-backed promotion judging contract
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 / PR Config AuditCommit: 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:
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 / PR Harness AuditCommit: Harness warnings detected (neutral) Scanned 4 changed config file(s) and found 1 harness issue(s).
Check publication was denied or unavailable. An app owner must enable Checks: read and write, and the installation owner must approve the updated permission. |
There was a problem hiding this comment.
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.
Sent by Cursor Approval Agent: Pull Request Approver
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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).
| { | ||
| mutationKey: ['writeTask'], | ||
| mutationFn: (t: QueuedTask) => | ||
| request(t.query, typeof t.variables === 'function' ? t.variables() : t.variables), |
There was a problem hiding this comment.
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.
| * request to re-send. */ | ||
| export function reportFailure(run: QueueRun, label: string, message: string): void { | ||
| clearDismiss() | ||
| if (!state.running) set({ ...IDLE, ...run }) |
There was a problem hiding this comment.
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.
| @@ -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' }) | |||
There was a problem hiding this comment.
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.
| concurrency: 1, | ||
| wait: 500, | ||
| started: true, | ||
| throwOnError: false, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| if (!node.content) continue | ||
| items.push({ | ||
| itemId: node.id, | ||
| number: node.content.number, |
There was a problem hiding this comment.
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>
| selection.clear() | ||
| if (more) { | ||
| setChoices(defaultChoices(details)) | ||
| setBodyOverride(null) | ||
| } else { | ||
| onClose() | ||
| } |
There was a problem hiding this comment.
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>
| 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[]>([]) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
| export function FieldEditor(props: FieldEditorProps): ReactNode { | ||
| const { field, items, initial, onCommit, onCancel, bar } = props | ||
| const [query, setQuery] = useState('') | ||
| const [draft, setDraft] = useState(initial?.display ?? '') |
There was a problem hiding this comment.
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>
| ))} | ||
| </ul> | ||
| <div className="rgp-queue-actions"> | ||
| <Button size="small" onClick={retryFailed}> |
There was a problem hiding this comment.
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>
| } | ||
| } | ||
|
|
||
| if (body.errors?.length) { |
There was a problem hiding this comment.
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>
| 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) { |
| if (error === null) { | ||
| switch (rule.tab) { | ||
| case 'replace': | ||
| next = re ? item.title.replace(re, rule.replaceText) : item.title |
There was a problem hiding this comment.
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>
| useLayoutEffect(() => { | ||
| if (!anchor) return | ||
| const rect = anchor.getBoundingClientRect() | ||
| const height = cardRef.current?.offsetHeight ?? 0 |
There was a problem hiding this comment.
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>
| // 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() |
There was a problem hiding this comment.
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>
| }, [selected.size]) | ||
|
|
||
| const items = allItems | ||
| const byNumber = useMemo(() => new Map(items.map((i) => [String(i.number), i])), [items]) |
There was a problem hiding this comment.
🔒 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.
Reviewed by Cursor Security Reviewer for commit f2e1252. Configure here.




Summary
Full rewrite of the extension on top of the WXT + Primer React v38 stack:
⌘K) covering every GitHub native field, relationship field (parent/blocked by/blocking), and project field, with option colors carried through.AsyncQueuerwrite queue (concurrency 1) with a live progress toast, retry, and dismiss.Dialog(one screen instead of a two-step wizard); everything carries over by default, with per-section toggles and an in-place title edit.Test plan
bun run test— 28 files / 190 tests greenbun run typecheck— cleanbun run lint— 0 errors (2 pre-existing warnings, unrelated files)bun run build:chrome/build:firefox/build:edge/ego-browserQA 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 hovercardSummary 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
Migration
bun install,bun run test|typecheck|build, and per-browser zips viabun run zip:chromeetc.pnpm-lock.yamlandpnpm-workspace.yamlare replaced bybun.lock.Written for commit f2e1252. Summary will update on new commits.