feat(search): cross-session FTS search for triage context and prior art - #324
Conversation
…6.1) sdd:6-verify's architecture review flagged that Story 1.6.1's GWT for the no-live-Instance worktree case describes an outcome the plan's own literal code sample never implements. Documents the deliberate shipped behavior (exclude rather than keep) and why the two GWTs aren't simultaneously satisfiable from entry.Project alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFjiqYD6vPT9TV3TATahh7
Extends SearchClaudeHistory/GetClaudeHistoryMessages/ListClaudeHistory with additive fields for session-level dedup, ±5-message context windows with bookends, best-effort automation-session exclusion (live Instance.Hidden), project scoping with worktree resolution, and anchor-based scroll paging. Surfaces a "Find related past work" search box in the backlog triage panel, pre-populated with the item title, session-deduped and repo-scoped, with click-through to the session's history page anchored on the matched message. - server/services/search_related_work.go: pure helper functions (dedup, context window, automation/project filters), reused by both SearchClaudeHistory and the ListClaudeHistory browse-mode path - proto: additive optional fields only, wire-compatible with existing callers - web-app: TriageRelatedWorkSection.tsx composed into TriageReviewPanel, useHistoryFullTextSearch.ts extended additively, history/page.tsx gains a ?sessionId=&messageIndex= deep link - backlog_debug_seed_handler.go: e2e-test-only `ended` seed flag so the triage e2e spec can exercise the live (non-readOnly) TriageReviewPanel Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFjiqYD6vPT9TV3TATahh7
The reviewer flagged that the e2e spec dropped the actual click-through assertion AC #10 requires ("an e2e test covers the triage panel's search-and-click-through flow end to end") — the prior spec only verified the search box auto-populates, deliberately skipping the click assertion because the real SearchClaudeHistory index build on a real dev machine's ~/.claude/projects tree is unbounded and non-deterministic. Adds a second e2e test that mocks the SearchClaudeHistory ConnectRPC call (page.route, an established pattern already used by vcs-widget.spec.ts) to deterministically produce one hit, then verifies clicking the result card opens a new tab at /history?sessionId=&messageIndex= and leaves the original tab's triage panel state untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFjiqYD6vPT9TV3TATahh7
✅ Registry ValidationTest Coverage: 27/181 features have
|
|
Closing as superseded: this branch's last known commit (654c601) is already present on main, so this item's work has already shipped through another path. No further fix is needed here. |
Go Benchmarks (Tier 1) |
E2E RPC Latency |
Frontend Terminal Throughput |
UX Analysis
|
🎬 E2E Feature Demos2 shard(s) recorded feature flows for this PR. recordings shard 1 Demo preview opens directly in browser (single-file HTML). Raw WebM recordings in ZIP. Expires after 30 days. |
- Fix TestGetClaudeHistoryMessages_AnchorIndexCentersWindow: it only asserted message count/length, which would pass even if anchor-centering were silently broken (all seeded messages had identical content). Now asserts actual window position via unique per-message content. - Remove a vacuous not.toHaveFocus() assertion in the empty-itemTitle test (nothing in the component ever sets focus, so it was always true) and add a real, dedicated focus-management test with populated results instead. - Drop includeContext:true from the triage panel's search query: the backend context_window/bookend fields it fetches are parsed by the hook but never rendered by SessionHitCard (v1 ships snippet-only cards per project_plans/session-search-fts5/design/ux.md) — was paying a full conversation-file read per hit on every debounced keystroke for unused data. - Parallelize per-hit context-window enrichment (enrichWithContext) so future callers that do set include_context don't pay N sequential full-file reads. - Extract SearchClaudeHistory's project/automation/dedup/truncate/context pipeline into applyResultPostProcessing, shrinking the handler and making the pipeline unit-testable independent of the RPC. - DRY up three identical filter-in-place loops (filterAutomationSessions, filterByProject, filterHistoryEntriesByAutomation) into a shared filterInPlace[T] generic, per this repo's own interface-pollution-checklist rule 5 (generalize once 2+ call sites need identical logic — now three do). - Reject offset>0 combined with group_by_session/exclude_automation_sessions/ project (previously just a documented, unenforced limitation) and reject anchor_index+tail combined (previously an unenforced "mutually exclusive" comment), each with CodeInvalidArgument, plus regression tests for both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFjiqYD6vPT9TV3TATahh7
|
Reopening — the auto-close was a false positive. |
…-session-search-fts5-ship # Conflicts: # web-app/src/gen/session/v1/session_pb.ts
✅ Registry ValidationTest Coverage: 27/181 features have
|
📊 Feature E2E CoverageFeature coverage report unavailable
|
✅ Registry ValidationTest Coverage: 36/186 features have
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds “related past work” discovery to backlog triage by extending history full-text search semantics (session-level dedup, project scoping, automation exclusion, optional context windows, and deep-link paging) and surfacing a new UI section that links directly to anchored history views.
Changes:
- Extend SearchClaudeHistory / GetClaudeHistoryMessages / ListClaudeHistory with additive request/response fields (dedup, context, automation exclusions, anchor-based paging).
- Add backlog triage UI section (“Find related past work”) powered by the enhanced FTS search and deep links into
/history. - Add unit + e2e coverage across client hook, new UI section, and server-side post-processing helpers.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-app/src/lib/hooks/useHistoryFullTextSearch.ts | Adds request flags + result mapping for session grouping, context, and automation exclusions. |
| web-app/src/lib/hooks/useHistoryFullTextSearch.test.ts | Adds tests ensuring new request flags + result fields are handled. |
| web-app/src/lib/features/features/backlog.ts | Registers the new triage-related-work feature in the feature catalog. |
| web-app/src/components/backlog/TriageReviewPanel.tsx | Injects the new TriageRelatedWorkSection into the triage panel (non-readOnly). |
| web-app/src/components/backlog/TriageReviewPanel.test.tsx | Stubs and asserts conditional rendering of the related-work section. |
| web-app/src/components/backlog/TriageRelatedWorkSection.tsx | New triage UI: debounced search + result cards linking to anchored history. |
| web-app/src/components/backlog/TriageRelatedWorkSection.test.tsx | Unit tests for prefill, debounce behavior, error/empty states, and link generation. |
| web-app/src/components/backlog/TriageRelatedWorkSection.css.ts | Styles for the triage related-work UI. |
| web-app/src/app/history/page.tsx | Adds deep-link handling via query params and Suspense-wrapped useSearchParams. |
| tests/e2e/triage-related-work.spec.ts | New Playwright specs for triage related-work behavior and click-through. |
| tests/e2e/pages/BacklogItemDetailPage.ts | Adds page object helpers + seed option for “ended” sessions. |
| server/services/search_service_test.go | Adds handler-level tests for new flags + anchor paging + validation. |
| server/services/search_service.go | Adds ListClaudeHistory automation filtering, anchor paging, oversampling, and post-processing pipeline hook. |
| server/services/search_related_work_test.go | Adds tests for grouping, context/bookends, automation filtering, project scoping, and paging. |
| server/services/search_related_work.go | New helper pipeline for post-processing search results and enriching context. |
| server/services/backlog_debug_seed_handler.go | Adds optional “ended” seed flag to support e2e triage panel state. |
| proto/session/v1/session.proto | Adds optional fields to keep wire compatibility while enabling new behaviors. |
| project_plans/session-search-fts5/implementation/plan.md | Documents an implementation-time correction for worktree scoping behavior. |
| docs/registry/features/frontend/ui/triage-related-work.json | Registers frontend feature doc with test IDs. |
| docs/registry/features/backend/history/search.json | Marks backend search feature as tested with new test IDs. |
| docs/registry/features/backend/history/list.json | Marks backend list feature as tested with new test IDs. |
Files not reviewed (1)
- gen/proto/go/session/v1/session.pb.go: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if start > 0 { | ||
| firstEnd := 3 | ||
| if firstEnd > len(messages) { | ||
| firstEnd = len(messages) | ||
| } | ||
| bookendFirst = messages[:firstEnd] | ||
| } | ||
| if end < len(messages) { | ||
| lastStart := len(messages) - 3 | ||
| if lastStart < 0 { | ||
| lastStart = 0 | ||
| } | ||
| bookendLast = messages[lastStart:] | ||
| } |
| const runSearch = (q: string) => { | ||
| // includeContext is deliberately omitted: v1 ships snippet-only cards | ||
| // (see project_plans/session-search-fts5/design/ux.md's fallback | ||
| // recommendation) — SessionHitCard never renders contextWindow/ | ||
| // bookendFirst/bookendLast, so requesting it would only cost the server | ||
| // an extra full-conversation-file read per hit on every debounced | ||
| // keystroke for data nothing displays. Re-add if a future card design | ||
| // actually surfaces the context window. | ||
| search({ | ||
| query: q, | ||
| project: repoPath, | ||
| groupBySession: true, | ||
| excludeAutomationSessions: true, | ||
| limit: 5, | ||
| }); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (!debouncedQuery.trim()) { | ||
| clearSearch(); | ||
| return; | ||
| } | ||
| runSearch(debouncedQuery); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [debouncedQuery, repoPath]); |
| it("useHistoryFullTextSearch_should_OmitNewFieldsFromResult_When_OptionsNotSet", async () => { | ||
| const { result } = renderHook(() => useHistoryFullTextSearch({ autoSearch: false })); | ||
|
|
||
| await act(async () => { | ||
| await result.current.search({ query: "auth refactor" }); | ||
| }); | ||
|
|
||
| await waitFor(() => expect(mockSearchClaudeHistory).toHaveBeenCalled()); | ||
| const [payload] = mockSearchClaudeHistory.mock.calls[0]; | ||
| expect(payload.groupBySession).toBe(false); | ||
| expect(payload.includeContext).toBe(false); | ||
| expect(payload.excludeAutomationSessions).toBe(false); | ||
| }); |
| func enrichWithContext(results []*sessionv1.SearchResult, hist *session.ClaudeSessionHistory) { | ||
| var wg sync.WaitGroup | ||
| for _, r := range results { | ||
| wg.Add(1) | ||
| go func(r *sessionv1.SearchResult) { | ||
| defer wg.Done() | ||
| msgs, err := hist.GetMessagesFromConversationFile(r.SessionId, 0) | ||
| if err != nil { | ||
| return // best-effort: leave context fields empty rather than failing the whole search | ||
| } | ||
| window, first, last := contextWindowAndBookends(msgs, int(r.MessageIndex)) | ||
| r.ContextWindow = toProtoClaudeMessages(window) | ||
| r.BookendFirst = toProtoClaudeMessages(first) | ||
| r.BookendLast = toProtoClaudeMessages(last) | ||
| }(r) | ||
| } | ||
| wg.Wait() | ||
| } |
…-session-search-fts5 # Conflicts: # gen/proto/go/session/v1/session.pb.go # web-app/src/gen/session/v1/session_pb.ts
✅ Registry ValidationTest Coverage: 40/188 features have
|
…-session-search-fts5 # Conflicts: # gen/proto/go/session/v1/session.pb.go # web-app/src/gen/session/v1/session_pb.ts
✅ Registry ValidationTest Coverage: 42/189 features have
|
…connect mock BacklogItemDetail.loadGuard.test.tsx mocked @connectrpc/connect down to just createClient, which silently clobbered ConnectError/Code once the merged TriageReviewPanel started always mounting TriageRelatedWorkSection (and its useHistoryFullTextSearch hook) in non-readOnly mode. The error-handling `err instanceof ConnectError` check then threw TypeError since ConnectError was undefined.
✅ Registry ValidationTest Coverage: 42/189 features have
|
Summary
When triaging a new backlog item, there's no way to see whether similar work has been attempted before — no visibility into prior decisions or failed approaches. This extends the existing BM25 history-search engine (
SearchClaudeHistory,GetClaudeHistoryMessages,ListClaudeHistory) with session-level dedup, ±5-message context windows, best-effort automation-session exclusion, project scoping (with worktree resolution), and anchor-based scroll paging — all additive, wire-compatible extensions, not a new RPC (per this repo's own architecture-review precedent: adding a parallelSearchSessionsRPC would have duplicated ~90% of the existing handler). Surfaces a "Find related past work" search box in the backlog triage panel.Closes backlog item
3141f49f-5ea7-4839-a848-5670635bff9e.What Changed
server/services/search_related_work.go(new): pure helper functions —groupResultsBySession,contextWindowAndBookends,isAutomationSession/filterAutomationSessions(also reused byListClaudeHistory's new automation filter),resolvedProject/filterByProject(worktree-aware via liveInstance.MainRepoPath)server/services/search_service.go: wires the above intoSearchClaudeHistory(project → automation → dedup → truncate → context-enrich, with oversampled raw fetch so a smalllimitdoesn't starve dedup) andGetClaudeHistoryMessages(newanchor_indexscroll paging + an out-of-bounds-offset bug fix), and addsexclude_automation_sessionstoListClaudeHistoryproto/session/v1/session.proto: additiveoptionalfields only onSearchClaudeHistoryRequest/Response,GetClaudeHistoryMessagesRequest,ListClaudeHistoryRequest— existing callers (useHistoryFullTextSearch.ts,HistorySearchResults.tsx) see byte-identical behavior when unsetweb-app/src/components/backlog/TriageRelatedWorkSection.tsx(new): the triage panel's search box — pre-populated with the item title, debounced, session-deduped, click-through opens/history?sessionId=&messageIndex=in a new tab via a real<a>elementweb-app/src/app/history/page.tsx: reads that deep link viauseSearchParams(wrapped inSuspense, matching this repo's ownbacklog/page.tsxprecedent)server/services/backlog_debug_seed_handler.go: small e2e-test-only addition (anendedflag) so the triage e2e spec can exercise the live (non-readOnly)TriageReviewPanelTest plan
go build ./...— cleango test ./server/services/...— all pass (dedup, context/bookend incl. a boundary-overlap regression, automation filtering on bothSearchClaudeHistoryandListClaudeHistory, project scoping incl. worktree resolution, anchor paging incl. out-of-bounds regression)go test ./...(full suite) — all packages passmake lint— 0 issuescd web-app && npx tsc --noEmit— cleancd web-app && npx jest --no-coverage— 268 suites pass (2 pre-existing failures unrelated to this change, confirmed viagit stashagainst the clean baseline)make registry-diff— 0% divergencecd tests/e2e && npx playwright test triage-related-work.spec.tsagainst a live isolated test server — both specs pass, including a deterministic click-through test (mocks the search RPC viapage.route, same patternvcs-widget.spec.tsalready uses) verifying the result card opens a new tab anchored on the message index without disturbing the triage panel's own statesdd:6-verifyran 4 parallel review agents (Go idioms, React idioms, architecture, refactor-candidates) — all MUST FIX findings resolved, including a real bookend-overlap correctness bug and an accessibility regression from a<label>/aria-labelconflict I'd introduced🤖 Generated with Claude Code
https://claude.ai/code/session_01QFjiqYD6vPT9TV3TATahh7