Skip to content

fix: quick-search surfaces connector captures + restore target flash#86

Merged
graydawnc merged 3 commits intomainfrom
fix/quick-search-includes-captures
Apr 15, 2026
Merged

fix: quick-search surfaces connector captures + restore target flash#86
graydawnc merged 3 commits intomainfrom
fix/quick-search-includes-captures

Conversation

@graydawnc
Copy link
Copy Markdown
Collaborator

Problem

Four bugs surfaced while QA'ing the provenance fix (PR #85) with a fresh Reddit sync:

  1. Quick-search preview never searched captures. The home dropdown ran searchSessionPreview only, so typing a term that only matched a connector item (e.g. a Reddit post title) returned "No quick matches yet" even though pressing Enter would find it in the All view. This was a pre-v0.1 oversight — the preview predates connectors and was never updated when captures were added.

  2. Session snippet was case-sensitive for its window position. buildLikeSnippet located the first hit with normalizedText.indexOf(term), but the upstream SQL uses LIKE which is ASCII case-insensitive. A lower-case query ("dark fantasy") would match messages containing "Dark Fantasy Realms", but the snippet builder couldn't find the hit position in the text and silently fell back to position = 0. Long messages ended up showing unrelated prefix text with the actual match truncated off. Highlighting via split(term) had the same blind spot.

  3. Clicking a dropdown suggestion no longer scrolled to the matched message. handleSelectSuggestion only received the session UUID — the messageId from the fragment result was thrown away, so SessionDetail's scroll-to-target effect never fired.

  4. The 2-second amber flash on the target message was gone. PR feat: scroll to matching message when opening session from search #30 added it; PR Add inline session find and session-to-search handoff #48 ("Make session detail searchable") incidentally removed the showHighlight state during the find-bar refactor and didn't put it back.

Changes

Preview merges captures with fragments

spool:search-preview now calls both searchSessionPreview and searchCaptures when no session filter is active. Fragments still get priority — captures only fill the remaining slots up to the limit — so users who never set up connectors see identical behavior to before. A source-scoped preview (claude/codex/gemini) stays sessions-only by intent.

The renderer pipeline propagates SearchResult[] end-to-end: preload return type, App.tsx state, and HomeView props are all unified. HomeView renders kind-specific rows:

  • Fragment: dot colored by session source + session title + matched snippet (with <strong> highlight)
  • Capture: dot colored by platform + capture title + platform · You saved this · author

Both row types now use the same SuggestionDot component, centered on the first-line text height (h-5 matches text-sm's 20px line-height) so the dot no longer appears vertically floating between the two lines.

Platform colors come from the connector manifest

PLATFORM_BADGE_COLORS was a hardcoded record in FragmentResults.tsx (and I initially duplicated it in HomeView.tsx — thanks for catching that). The connector SDK already requires every connector to declare color in its package.json spool metadata, and ConnectorStatus already ships it to the renderer. Now App.tsx builds a platform → color map from window.spool.connectors.list() and passes it down as a prop. Unknown platforms fall through to the accent color instead of a silent default.

Case-insensitive snippet

buildLikeSnippet lowercases both the text and the terms before indexOf to locate the first hit, then uses a case-insensitive regex (new RegExp(escapeRegex(term), 'gi')) to wrap matches in <mark> while preserving the original casing in the displayed text. Regex metacharacters in terms are escaped defensively (e.g. v1.2.3 now highlights correctly).

Dropdown click plumbs messageId

handleSelectSuggestion now accepts (uuid, messageId?) and forwards both to setTargetMessageId, matching what the All view's handleOpenSession has always done.

Target-message flash restored

SessionDetail re-adds showTargetHighlight state with the 2s setTimeout, using the same transition-colors duration-700 bg-accent/10 class from PR #30. The target message also gains stable data-testid="target-message" and data-highlighted="1" attributes so e2e tests don't have to pattern-match Tailwind class strings.

Tests

Unit — packages/core/src/db/search-query.test.ts

4 new tests for buildLikeSnippet:

  • Window centers around the first hit with case-insensitive matching (pre-fix behavior fell back to position 0 and sliced off the real match)
  • <mark> preserves original casing when query and text differ in case
  • Empty input returns empty string
  • Regex metacharacters in terms (1.2.3) are escaped and match literally

E2E — packages/app/e2e/home-preview.spec.ts

4 new tests:

  • Capture-only query (ZZQCAPTURE_ONLY_UNIQUE, seeded via sqlite3 CLI) surfaces a data-kind="capture" row in the dropdown
  • Session suggestion second line contains <strong> with the matched term
  • Lower-case query highlights mixed-case text (case-insensitive regression guard)
  • Clicking a fragment suggestion lands on data-testid="target-message" with data-highlighted="1", which disappears within 5 seconds (the 2s fade timer plus 3s of slack)

Non-flakiness notes

  • Assertions key on stable data-testid / data-* attributes, never on Tailwind class regex
  • Captures are seeded via the sqlite3 CLI, avoiding the node/electron ABI mismatch that would happen if the test process tried to load the app's better-sqlite3 (which gets rebuilt for electron in the test:e2e script)
  • toHaveAttribute / not.toHaveAttribute use Playwright's auto-retry, so the 2s timer doesn't need an explicit sleep
  • Existing 12 fast-search.spec.ts tests still pass

Test plan

  • pnpm --filter @spool/core test — 151 passed (was 147, +4 new)
  • npx playwright test e2e/home-preview.spec.ts — 4/4 passed (5.2s total, flash test 2.9s)
  • npx playwright test e2e/fast-search.spec.ts — 12/12 passed, no regression
  • Type-check clean across @spool/core, @spool/app, @spool/cli
  • Manual: home dropdown surfaces Reddit "Dark Fantasy Realms" for dark fantasy query; clicking session result jumps with amber flash

The home dropdown ignored connector captures entirely, so searching a term
that only matched a Reddit/HN/etc. post returned "No quick matches yet"
even though pressing Enter would find it in the full view. The snippet
generator was also case-sensitive on its window-placement heuristic — so
typing "dark fantasy" never found the "Dark Fantasy Realms" hit and just
sliced from position 0, hiding the match. Clicking a dropdown suggestion
had dropped the message-id plumbing, and the 2-second amber flash on the
target message (PR #30) had been removed incidentally in PR #48.

Changes:
- spool:search-preview IPC merges searchSessionPreview + searchCaptures;
  sessions still get priority, captures fill remaining slots so search
  behaviour for session-only users is unchanged
- Renderer types propagate SearchResult[] end-to-end; home dropdown
  renders kind-specific rows (dot + title + snippet/source line), capture
  rows link out to the URL
- Platform colors now come from the connector manifest via
  connectors.list() instead of a hardcoded map that silently fell through
  to accent for unknown plugins
- buildLikeSnippet lowercases both sides for indexOf and uses a case-
  insensitive regex for <mark> wrapping, preserving original casing
- handleSelectSuggestion accepts messageId so home-dropdown clicks land
  on the exact matched message
- Restore showTargetHighlight state + 2s timer removed in PR #48; expose
  data-highlighted attribute for stable e2e assertions

Tests:
- 4 unit tests for buildLikeSnippet (case-insensitive window, case-
  preserving highlight, empty input, regex-metachar safety)
- 4 e2e tests covering capture-only query, session snippet highlight,
  case-insensitive search, and click-to-flash — flash assertion keyed on
  data-highlighted attribute (not Tailwind class) to stay non-flaky
- Captures seeded via sqlite3 CLI to avoid node/electron ABI mismatch
PR #86 only covered the home dropdown click path. Add matching coverage
for the two other entry points that flow through handleOpenSession:
- Clicking a row directly from the All view
- Clicking a row after applying a source filter (Claude Code tab)

Both paths share the same handler, but the UI surface differs; catching
a regression in either one previously required manual testing.
macOS GitHub runner ships sqlite3 without FTS5, so the captures_fts
INSERT trigger failed when the CLI tried to write a seeded row.

Install a globalThis.__spoolSeedCapture hook in main/index.ts when
SPOOL_E2E_TEST=1 is set (launchApp always sets it). The hook reuses
the app's already-loaded better-sqlite3 (which bundles its own SQLite
with FTS5 regardless of the OS) and the live db handle, so there's
no ABI mismatch and no second connection.

Dynamic import and process.mainModule.require were both tried first
but neither is available in Playwright's evaluate sandbox — a named
globalThis hook is the only approach that works with stock Electron.
@graydawnc graydawnc merged commit 5efac9a into main Apr 15, 2026
3 checks passed
@graydawnc graydawnc deleted the fix/quick-search-includes-captures branch April 15, 2026 07:04
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