AI Chat: support searching text within a chat session
Author: safisaCreated Sep 10, 2026Updated Sep 15, 2026
Labelstheia-ai
Feature Description:
There is currently no way to search the text of an AI chat session. Long sessions can only be navigated by scrolling, and content that is off-screen cannot be found at all.
Current state:
- The chat view registers no find command (
packages/ai-chat-ui/src/browser/chat-view-commands.ts), and the tree's type-ahead search is explicitly disabled (packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-container.ts,search: false). - The chat view is a virtualized
TreeWidget(react-virtuoso), so the browser's native find cannot see off-screen turns. A DOM-based find widget would have the same limitation.
Proposal (step 1, VS Code parity):
Ctrl+F/Cmd+Fwhile the chat view is focused opens an incremental find bar with a match count and previous/next navigation (Enter/Shift+Enter,Esccloses). The terminal find bar (packages/terminal/src/browser/search/terminal-search-widget.tsx) is the closest existing UI to build on; the treeSearchBoxis a type-ahead overlay without an input element and does not fit. Match Case / Whole Word / Regular Expression toggles come with that design and are included.- Matches are computed from the session model rather than the DOM, so off-screen content is covered. Request text is searched, and response content is searched for text, markdown, code, error and informational parts. Tool calls, reasoning and delegated agent sub-chats are excluded, matching VS Code. The generic
ChatResponseContent.asString()/asDisplayString()accessors (packages/ai-chat/src/common/chat-model.ts) only fit text and markdown:CodeChatResponseContent.asString()wraps the code in fences with the language id,InformationalChatResponseContent.asString()returnsundefinedand has noasDisplayString(), andErrorChatResponseContentonly providesasDisplayString()with the raw error message. The search text is therefore extracted per kind (code,content.value,error.message). Amermaidcode block is the one exception among code parts:MermaidPartRenderer(packages/ai-chat-ui/src/browser/chat-response-renderer/mermaid-part-renderer.tsx) renders it as a diagram rather than as source, so the source text is nowhere on screen and a match in it would be counted but could never be highlighted or revealed. It is therefore not searched. Relatedly, the rendered diagram carries its stylesheet in an SVG<style>element, so whatever walks the DOM to highlight must skip<style>and<script>subtrees. - Error parts are the only included content that can be collapsed:
error-part-renderer.tsxsplits the message viaformatProviderError()into a headline and a collapsed<details>block holding the provider's JSON body. Searching only the formatted headline message keeps this step free of expand-on-reveal logic. - Find is unavailable while the session has no requests. That state renders the welcome screen, which also hosts the session list (
ChatSessionsWelcomeMessageProviderin@theia/ai-ide), so there is nothing to search; an open find bar closes when the tracked session is replaced by one without requests. - Revealing a match scrolls to the owning row (via the existing
scrollToRowhandling inChatViewTreeWidget) and highlights the matched text once the row is rendered, e.g. with the CSS Custom Highlight API so the React-rendered DOM is left untouched. Because the tree is virtualized the two differ in reach: the match count covers the whole session, while highlights can only exist for mounted rows and appear as rows scroll into view. Scrolling to a match must move the tree's own scroll container rather than callElement.scrollIntoView(), which scrolls every scrollable ancestor up to the viewport and shifts the application shell. - The keybinding is scoped with the existing
chatResponseFocus/chatInputFocuscontext keys.Ctrl+Fcurrently has two competing bindings: core's always-enabled no-opCommonCommands.FIND(packages/core/src/browser/common-frontend-contribution.ts) and Monaco'sactions.find, registered witheditorFocus || editorIsOpenand enabled whenever a Monaco editor was focused last (the chat input is one). Theia'sKeybindingRegistryresolves ties between enabled bindings by registration order within a scope, later registrations first, and the chat UI module registers after both, so the chat-scoped binding takes precedence;ChatInputPasteContributionalready relies on this to override Monaco'sCtrl+V.Ctrl+Finside the chat input therefore opens the chat find rather than Monaco's find within the one-line input, matching VS Code. - Matches are recomputed while a response streams. Only the displayed branch of the session is searched.
- The model holds markdown source while the user sees rendered text, so link targets and formatting characters are matched although not visible. Acceptable for a first step; may cause small count differences.
Follow-ups (out of scope for step 1):
- Include tool call arguments/results, reasoning and tool confirmations, behind a scope toggle. Each needs its own text extraction:
ToolCallChatResponseContent.asString()returns an empty string andasDisplayString()omits the result;ThinkingChatResponseContent.asString()returns JSON including the signature; and a tool call result is typedundefined | object | string | ToolCallContent(packages/ai-core/src/common/language-model.ts), so it needs per-shape handling plus a size cap, otherwise a single large file read floods the match count. Confirmation text is the awkward case: it is composed in the renderer from the tool request, localized strings and rendered markdown (packages/ai-chat-ui/src/browser/chat-response-renderer/tool-confirmation.tsx) rather than stored on the model, so a model-side search needs a model-level accessor for it first. - Hoist collapsible state out of the DOM, which is the prerequisite for revealing a match inside a collapsed block. Every collapsible block is an uncontrolled
<details>with noopenprop and no persisted state (reasoning, finished tool calls, server tool calls, error details, compaction, delegation), and the virtualized tree unmounts off-screen rows, so a remounted block renders collapsed again. Widget-side expanded state keyed by content id fixes this, but response parts are rendered by contributedChatResponsePartRenderers, so it is a contribution-point change rather than a local one. - Prefer a scope toggle over tying the search scope to what is currently expanded. With a virtualized tree the latter makes the match count change when a row scrolls out of view, not only when the user expands or collapses something. VS Code does not tie scope to expansion either: it fixes a scope and expands a collapsed container when revealing a match inside it.
- Search delegated agent sub-chats at all nesting levels. The child
ChatModelis only reachable viaAgentDelegationTool.getDelegation(id).invocation.responseCreated(packages/ai-chat/src/browser/agent-delegation-tool.ts), and delegations are tracked in memory, so for a restored session the child sessions would first need to be loaded fromChatSessionStore. Related: #16881.
Prior art:
- VS Code Copilot Chat:
Ctrl+Fin the chat view searches prompts and responses including off-screen content and code blocks,Enter/Shift+Enternavigate matches, and navigating into a collapsed section expands it. Tool pills and collapsed reasoning are excluded. - Zed: thread history search matches thread titles only, not content.
Source: eclipse-theia/theia