Skip to content

feat: find threads by PR number in the command palette - #2294

Merged
jonathanKingston merged 5 commits into
mainfrom
copse/can-we-make-the-cmd-shift-k-search-also-fi-08afd2
Sep 4, 2026
Merged

jonathanKingston merged 5 commits into
mainfrom
copse/can-we-make-the-cmd-shift-k-search-also-fi-08afd2

Conversation

@jonathanKingston

Copy link
Copy Markdown
Collaborator

Summary

Cmd/Ctrl+Shift+K now finds a thread by the GitHub PR it touched. Searching 2262, #2262, copse-dev/agent-pane#2262, or a pasted PR URL all narrow to the thread that opened #2262.

Why

The palette matched threads on title and project name only, so the thread behind a PR was unfindable by the number everyone actually refers to it by.

The data was already on disk. Thread.prRefs caches every PR a thread touched — scraped from message prose, recorded directly by gh_pr_create, and filled in from remoteAgentLink.prUrl for cloud-agent runs. It just never reached the palette: the palette reads catalog.jsonl, and ThreadCatalogEntry dropped the field.

What changed

Index the refs. ThreadCatalogEntry carries prRefs. catalogEntryFromDisk already reads the meta.json that holds them, so this costs no extra I/O and no transcript fold.

Match without a parser. PR keys are owner/repo#number, so 2262, #2262 and copse-dev/agent-pane#2262 all hit as plain substrings. A pasted PR URL is the one spelling that would not, so queryTerms folds it through the existing parseGithubPrUrl to that same shape — a link copied out of the PR pane and the bare number from the same PR should not disagree.

Show why a row matched. Thread rows render a #2262 chip; without it a bare-number query returns a title with no visible reason for the hit.

Migration, and the hazard it created

The new field is required, not optional, so readCatalog rejects every line of a pre-prRefs catalog. Those lines then look like missing threads to catalogMissingIndexedThreads, which reuses the existing rebuildCatalogFromDisk to backfill. Without that, every thread predating the field would stay silently unsearchable by PR number.

That rejection has a sharp edge worth reviewing closely: upsertCatalogEntry and refreshCatalogLine read the catalog and write the whole map back, so the first message append against an old file would have persisted a one-line index and dropped every other thread. Both now read through ensureCatalogMap, which rebuilds from thread dirs first. There is a regression test that appends a message against a stale two-thread catalog and asserts both survive.

Notes for reviewers

  • Threads are still matched by title, so a title containing 2262 hits too. That is existing substring behaviour, not a regression.
  • The @-thread mention picker shares loadProjectCatalog and picks up PR matching as well.
  • matches() no longer re-splits the query per candidate row; terms are computed once per keystroke.
  • refreshCatalogLine now costs one extra readdir per message append (via ensureCatalogMap), alongside the catalog read and write it already did.

Issue numbers are deliberately out of scope

Nothing stores a GitHub issue against a thread today — the only issue refs in the codebase live in roadmap note frontmatter (parseIssueRef), never on thread metadata. Covering issues needs a new persisted field, a scraper widened to /issues/, and a decision on bare #123, which GitHub itself resolves to either a PR or an issue. That is its own piece of work.

Validation

  • npm run check — typecheck, lint, format, dead-code, oracle and e2e-syntax all clean. Unit suite: 8240/8262 passed. The 13 failures are the known host-flaky SSH/askpass/socket-path and workspace-indexing set (ssh-workspace/openssh-transport, ssh-workspace/ssh-paths, ssh-workspace/askpass, search/workspace-indexing); none of those files reference anything in this diff, and they reproduce identically in isolation.
  • npm test -- thread-store — 83 passed
  • npm test -- command-palette mention-picker — 19 passed

🤖 Generated with Claude Code

Co-Authored-By: Copse noreply@copse.dev
Copse-Models: acp:claude-acp#opus[1m]

Cmd/Ctrl+Shift+K matched threads on title and project name only, so the
thread that opened a PR was unfindable by the number everyone actually
refers to it by.

The data was already on disk — `Thread.prRefs` caches every GitHub PR a
thread touched — but it never reached the palette, because the palette
reads `catalog.jsonl` and `ThreadCatalogEntry` dropped the field. So
carry `prRefs` on the catalog entry: `catalogEntryFromDisk` already
reads the `meta.json` that holds it, making this free of extra I/O and
of any transcript fold.

PR keys are `owner/repo#number`, so `2262`, `#2262` and
`#2262` all match as plain substrings with no query
parsing. A pasted PR URL is the one spelling that would not, so
`queryTerms` folds it through the existing `parseGithubPrUrl` to that
same shape — a link copied out of the PR pane and the bare number from
the same PR should not disagree. Thread rows now show a `#2262` chip:
without it a bare-number query returns a title with no visible reason
for the hit.

The new field is required rather than optional so that `readCatalog`
rejects every line of a pre-`prRefs` catalog. Those lines then look like
missing threads to `catalogMissingIndexedThreads`, which reuses the
existing `rebuildCatalogFromDisk` to backfill — otherwise every thread
predating the field would stay silently unsearchable by PR number.

That rejection has one hazard: `upsertCatalogEntry` and
`refreshCatalogLine` read the catalog and write the whole map back, so
the first message append against an old file would have persisted a
one-line index and dropped every other thread. Both now read through
`ensureCatalogMap`, which rebuilds from thread dirs first.

Threads are still matched by title, so a title containing `2262` hits
too. The `@`-thread mention picker shares `loadProjectCatalog` and picks
up PR matching as well.

Issue numbers are deliberately not covered: nothing stores a GitHub
issue against a thread today (the only issue refs live in roadmap note
frontmatter), so that needs a new persisted field and a decision on
ambiguous bare `#123`, which GitHub itself resolves to either kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@copse-release-bot

copse-release-bot Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Reference screenshots ready for review

Review GitHub’s image diffs in screenshot PR #2339.
Merge it (or enable auto-merge) to apply the accepted PNGs to copse/can-we-make-the-cmd-shift-k-search-also-fi-08afd2.

Rendered for 4a3af65aa29c by CI run 33860133909; the immutable artifact is reference-screenshot-candidates-33860133909.

If this source branch moves, a later successful render closes the stale review PR and replaces this link.

@jonathanKingston jonathanKingston left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: find threads by PR number

Verdict: changes needed before merge. The migration design holds up under probing, but the branch now conflicts with main and the visible change has no screenshot evidence.

Validation

Check Result
pnpm run typecheck pass
oxfmt --check on the 7 changed files pass
eslint on changed .ts files pass
pnpm test -- thread-store command-palette mention-picker 102/102 pass
pnpm run check:dead-code pass
Ad-hoc probe: pre-prRefs catalog with 3 threads, then deleteProjectThread, updateMeta({archivedAt}), updateMeta({title}), a mixed old/new file, and a malformed prRefs line no entry lost in any case; rebuild persisted once
Dry merge with origin/main CONFLICT in src/main/services/thread-store.ts and src/shared/types/thread.ts

Blocking

  1. Rebase onto main. #2317 moved the store into packages/thread-store: src/main/services/thread-store.ts is now a 5-line re-export and ThreadCatalogEntry lives at packages/thread-store/src/thread-types.ts:296. The existsSync ? readCatalog : rebuild pattern this PR fixes is still present in the package copy (upsertCatalogEntry at packages/thread-store/src/thread-store.ts:876, refreshCatalogLine at :923), so the fix has to be re-applied there rather than in the shim.

  2. Visual evidence. The #2262 chip (command-palette.ts:229-237, command-palette.css:72-80) and the new placeholder (command-palette.ts:118) are user-visible, but tests/ is untouched. tests/e2e/command-palette.e2e.ts seeds four threads with no prRefs and types login, so command-palette.png cannot show either. #2296 touches 12 screenshots and none is command-palette.png, so it is unrelated rendering drift, not evidence for this PR. seedThreadDir writes meta.json from the seed object verbatim, so adding prRefs to one seeded thread, searching by number, asserting .command-palette-pr, and saving a screenshot is a small addition.

Migration analysis

Every catalog reader-then-writer on the head:

  • upsertCatalogEntry (:904) and refreshCatalogLine (:954) go through ensureCatalogMap. Safe.
  • saveProjectThreads (:1331) writes wholesale from its argument by design. Safe.
  • deleteProjectThread (:1631-1637) is the only remaining direct readCatalog + writeCatalog. Against an all-stale file readCatalog returns an empty map, the delete returns false and nothing is written; against a mixed file it writes only the new-format lines. Both self-heal because catalogMissingIndexedThreads checks every non-archived thread dir against the map. No data loss, but routing it through ensureCatalogMap would make the invariant uniform.
  • A malformed prRefs line drops that line, which triggers rebuild from meta.json. Rebuild cost is meta.json plus parseSpine of events.jsonl for the first user message, pre-existing and one-off.

Required rather than optional is defensible: the catalog is derived and rebuildable, and optional-with-[] would leave every existing thread silently unsearchable by PR with no backfill. Older builds reading the newer catalog ignore the key and drop it on write; the newer build then rebuilds.

Non-blocking

  • upsertCatalogEntry and refreshCatalogLine now run catalogMissingIndexedThreads on every append and meta patch: a readdirSync plus a meta.json read per archived thread. Projects with many archived threads pay that per write. Having readCatalog report rejected lines would keep the rebuild trigger precise without the scan.
  • Substring matching means 2262 also hits #22620 and any title containing 2262, and rows sort by updatedAt with no preference for the exact PR. Anchoring numeric terms to the end of the key would fix it.
  • Main-side loadProjectCatalog (:1899) does not fold PR URLs; only the renderer queryTerms does, so api.threads.catalog(id, url) from the mention picker returns nothing for a pasted link.
  • queryTerms does not strip trailing punctuation from a pasted URL the way extractGithubPrUrls does, so .../pull/2262. stays a raw URL and matches nothing.

Generated by Claude Code

…fi-08afd2

Resolves conflicts from #2317 (thread store extracted to @copse/thread-store):
take main's re-export shims for src/main/services/thread-store.ts and
src/shared/types/thread.ts, and re-apply this branch's catalog prRefs
changes inside packages/thread-store/src/.
`deleteProjectThread` was the last catalog writer reading the index with
`readCatalog` directly. Against a pre-`prRefs` catalog — every line of which
this build rejects — that read is empty, so the delete found nothing to remove
and, had it written, would have replaced the index with nothing. Route it
through `ensureCatalogMap` like `upsertCatalogEntry` and `refreshCatalogLine`,
so all three writers share one rebuild-on-read invariant, and add the matching
regression test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB
…with punctuation

Two follow-ups to PR-number thread search:

- A PR URL pasted out of prose keeps its sentence punctuation
  (`…/pull/2262.`), which `parseGithubPrUrl` rejects. `queryTerms` now strips
  it the same way the transcript scraper does, via a shared
  `stripUrlTrailingPunctuation`, so the pasted link and the bare number agree.
- A numeric term (`2262`, `#2262`) matched PR keys as a substring and so also
  surfaced #22620. `githubPrKeyMatchesTerm` pins a numeric term to the whole
  number while non-numeric terms (`owner/repo#2262`, `agent-pane#`) still match
  as substrings. Title/digest/project-name substring matching is unchanged.
  Applied in both the palette and `loadProjectCatalog` so the two agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB
Seed the target thread with a `prRefs` entry in the shape `gh_pr_create`
records on `meta.json`, query its bare PR number, assert the single matching
row and its `.command-palette-pr` chip, and save
`command-palette-pr-number.png` (a new reference; `command-palette.png` is
untouched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jaQbed96h8aSZLijfiHNB

Copy link
Copy Markdown
Collaborator Author

Pushed four commits. e19a22a merges main, with the PR's catalog changes re-applied inside packages/thread-store/src/ (prRefs on ThreadCatalogEntry, the parseCatalogPrRefs decoder, ensureCatalogMap routing for upsertCatalogEntry and refreshCatalogLine, the PR-key haystack in loadProjectCatalog); the original diff was saved and checked so nothing was dropped. 912857d routes deleteProjectThread through ensureCatalogMap too, with a stale-catalog delete test. faf1597 makes an all-digit or #digits term match #<n> only at the end of a key (so 2262 no longer hits #22620) in both the palette and main's loadProjectCatalog, and strips trailing punctuation from pasted URLs via a shared stripUrlTrailingPunctuation. 4a3af65 extends tests/e2e/command-palette.e2e.ts with a seeded prRefs thread, a 2262 search asserting the #2262 chip, and a new command-palette-pr-number.png screenshot (CI renders it; no PNG committed). Typecheck, eslint, oxfmt, dead-code, e2e-syntax, oracle and 296 targeted tests pass.


Generated by Claude Code

github-actions Bot added a commit that referenced this pull request Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🖥️ PR preview

@jonathanKingston
jonathanKingston merged commit 791776e into main Sep 4, 2026
23 checks passed
@jonathanKingston
jonathanKingston deleted the copse/can-we-make-the-cmd-shift-k-search-also-fi-08afd2 branch September 4, 2026 20:40
github-actions Bot added a commit that referenced this pull request Sep 4, 2026

This branch was previously deployed

1 inactive deployment
github-pages — 4a3af65a Deployed Sep 4, 2026 by jonathanKingston via deploy / deploy #3435
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.

2 participants