feat: full-text search over past conversations - #618
Open
FelixWang47831 wants to merge 3 commits into
Open
Conversation
The sidebar could only browse sessions per project; finding an old conversation by what was said in it required grepping ~/.pi/agent/sessions by hand. Add a search toggle (Ctrl/Cmd+Shift+F) that replaces the session list with a debounced full-text search over stored session JSONL files, with highlighted snippets, per-session hit counts, project scoping, and an opt-in for tool output. Clicking a result opens that session through the normal select path, so results from another project move the effective cwd like any other session click. lib/session-search.ts streams the files instead of indexing them: there is no index to keep in sync, candidate sessions come from listAllSessions() so project scoping matches the sidebar and no request can name a file outside the sessions directory, and the scan is bounded by a file cap, a wall-clock budget, and a per-session hit cap that are all reported back through stats. Signed-off-by: Felix Wang <fei.wang_3@nxp.com> Assisted-by: Pi:claude-opus-5
Conversation search could only open a session at its newest page, so a hit in a long history still had to be found by scrolling. Clicking a snippet now opens the session at that message: it is scrolled to the middle of the viewport and flashed once (the result header still opens at the first hit). Snippets became their own buttons inside the result row instead of one button around everything. Reaching an old entry has to respect the chat's paging model: loaded messages are a contiguous run ending at the leaf, and only upward paging exists. So `?from=<entryId>` returns everything from that entry, plus a short lead-in, through the leaf — a superset of the normal tail page — and refuses the jump when the entry is unknown, on another branch, or farther back than MAX_JUMP_ENTRIES, in which case the session still opens on its newest page. The client completes the jump over up to three renders, each waiting on the state the previous one changed: fetch, then the render window catching up, then scroll and flash. Tool results and collapsed process messages have no row of their own, so lib/chat-jump.ts resolves a target to the nearest earlier row through messageRefs. Signed-off-by: Felix Wang <fei.wang_3@nxp.com> Assisted-by: Pi:claude-opus-5
Jumping to a hit still left the reader scanning a long message for the keyword it matched on. The jumped-to message now highlights every occurrence of the query and the chat scrolls to the first one instead of to the message. The highlight outlives the row flash — it is what makes a long message readable — and is dropped on the next jump or when the chat unmounts. It is painted through the CSS Custom Highlight API rather than by wrapping text in <mark>: the message DOM belongs to React (markdown, code blocks, tool output), and splitting its text nodes would invalidate the node references React diffs against. Ranges live outside the DOM, so a re-render can only drop the highlight, never corrupt the content. Where the API is missing (Firefox < 140) the row flash still works. The ::highlight() rule is injected from lib/text-highlight.ts and kept self-contained. The build CSS parser does not recognize the pseudo-element, and highlight pseudo-elements resolve custom properties through the highlight inheritance chain rather than from the originating element — so a var(--theme-color) there can stay unresolved, and an unresolved var drops just that declaration, which shows up as a keyword that has the underline but no background. Literal colors plus an explicit foreground keep a strong yellow legible in both themes, and an existing style tag is rewritten rather than skipped so a tab that survives a rule change stops painting the old colors. Matching moved to lib/text-match.ts because lib/session-search.ts reaches node:fs through session-reader and cannot be imported by the browser bundle; the panel passes its own query, mode, and case sensitivity into the jump target so the chat highlights exactly what the server matched. Signed-off-by: Felix Wang <fei.wang_3@nxp.com> Assisted-by: Pi:claude-opus-5
FelixWang47831
marked this pull request as ready for review
August 26, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR description
The sidebar can browse sessions per project, but finding a conversation by what
was said in it means grepping
~/.pi/agent/sessionsby hand. A magnifier next toNew / Refresh (or
Ctrl/Cmd+Shift+F) replaces the session listwith a debounced full-text search over the stored session JSONL files: results
are grouped per session with highlighted snippets, scopable to one project and
optionally including tool output, and clicking one opens that session at that
message, scrolls to the first keyword occurrence and highlights every occurrence
in it. Adds
GET /api/sessions/searchand?from=<entryId>on the contextroute; i18n for
en/zh-CN/zh-TW; no new dependencies.lib/session-search.tsstreams the files on every keystroke,takes its candidates from
listAllSessions()(so project scoping matches thesidebar and no request can escape the sessions directory), and is bounded by a
file cap, an 8s budget and a per-session hit cap that it reports back so the UI
can say "narrow the search".
?from=returns everything from the target entry through the leaf, and refusesthe jump (unknown entry, other branch, too far back) instead of shipping an
isolated window.
<mark>would split text nodesReact owns, so
lib/text-highlight.tsuses the CSS Custom Highlight API(Chrome/Edge 105+, Safari 17.2+, Firefox 140+; elsewhere it degrades to the row
flash), and
lib/text-match.tsis new so client and server share one matcher.45 tests added,
tsc --noEmitandnpm run lintclean, and the remainingnpm testfailures also fail on an unmodified checkout (Windows CRLF andPATHassertions).