Repository navigation
feat(web): global search palette — find chats, workspaces and projects, run commands, toggle settings - #804
Open
danyaberezun wants to merge 18 commits into
Open
danyaberezun wants to merge 18 commits into
danyaberezun wants to merge 18 commits into
Conversation
danyaberezun
requested review from
Olga Lavrichenko (OLavrik),
Rustam Sadykov (SBOne-Kenobi) and
Rinat S (rsolmano)
as code owners
October 22, 2026 03:38
5 tasks done
5 tasks done
Collaborator
Author
|
CI note: the only failing job is E2E (no-agent) shard 1/2, always on |
danyaberezun
marked this pull request as draft
October 22, 2026 05:58
danyaberezun
force-pushed
the
global-search-research
branch
from
October 26, 2026 00:15
15ff56a to
75e1550
Compare
danyaberezun
force-pushed
the
global-search-research
branch
from
October 26, 2026 19:50
de3b7cc to
4802244
Compare
This was referenced Oct 26, 2026
danyaberezun
force-pushed
the
global-search-research
branch
from
October 28, 2026 03:25
4802244 to
b14c1d7
Compare
danyaberezun
marked this pull request as ready for review
October 28, 2026 05:36
`Mod+K`, double Shift and a topbar button open one palette that finds chats, workspaces and projects across every open project, runs commands with their shortcuts shown, deep-links Settings sections and toggles MCP servers and extensions in place. The empty query keeps the former switcher's role as the "Waiting for you" group next to Recent and Actions. Two new web sub-modules: `commands` (the typed contract an invocable action is described with; handlers stay with their owners) and `search` (props-driven palette, prefix/filter grammar, tiered ranking over the shared matcher with decay-free frecency and learned query→pick, client-local preferences, the double-Shift state machine). Shell composes the catalog, pages and row actions and injects them; `panels` import neither module.
…ions and module specs Also the README capability line for the palette.
…es in the search spec
…ne terminal, IME, protocol gates, quoted filters - Chat-title backfill now covers every untitled workspace (active and context-project first) four requests at a time, re-asks a workspace at most once a minute and keeps the titles across opens, instead of stopping at the first 24 forever. - `terminal.new` is a host mutation (placement reserves on the host), so it is disabled offline. - The action panel's Enter ignores an IME composition like the main input does. - `settings.extensions` / `settings.mcp` follow the same protocol gates as their Settings sections, and the extension catalog / MCP list are requested only when the host serves them. - Quoted spans are tokenized before state filters, so `"is:running"` stays literal text.
… non-English catalogs
Drop the inline Theme, Extensions and MCP pages with their page stack, the per-row action panel, in-place toggles and the is:/has: filter grammar. Every setting row now opens its Settings section, the double-Shift row included; Mod+K inside the palette closes it; a command always runs.
… keep the title backfill current The palette's backfill now loads the workspace list of every open project it has not seen — not only projects that already have session-state records — so a collapsed project's workspaces are searchable, and it re-reads its queue whenever the normalized snapshot gains or loses a workspace, so a palette opened during cold-start hydration catches up.
…s the retired switcher did
…eys, IME, bounded backfill and learning, literal quotes, Settle gate - Keys live on the dialog frame, not the input: a click on a header, the footer or the preview no longer leaves Escape, Mod+K and the arrows dead; Escape comes through Radix's `onEscapeKeyDown`, so a synthetic close-item Escape still closes and a nested layer keeps its own. - An active IME owns every key (`isComposing` or the 229 sentinel, also used by double Shift); plain Ctrl+R only is swallowed, so Ctrl+Shift+R reloads. - Title backfill: one `session.list` per workspace until its session set changes (a failed read included), workspaces whose chats are all titled are skipped, `workspace.list` skips diff stats, and both queues keep four requests in flight across palette openings (`backfillQueue`, unit-tested); the queues re-read on welcome, open-project and workspace changes. - Learning: adaptive counts are computed once per query and keyed on the text without its scope prefix; a query keeps its 32 most recent picks. - Matcher: a quoted term is a contiguous literal (no acronym or subsequence), an unterminated quote runs to the end, and case plus Latin accents fold without moving highlight offsets (İ, final sigma). - Waiting for you leaves running chats out and shows eight rows before an in-place "Show N more"; a notification click resets an already-open palette (`searchSession`). - Settle is disabled with the tree's reason while an agent works or a chat needs you (`Command.disabledReason`); the palette offers Start a new project and Open a path on the host, shows the native Settings shortcut, and says Connecting rather than Disconnected before the first welcome. - Sections are named groups; the chips and the Preview toggle use the shared Button (`aria-pressed`). - Tests: matcher, learning, sections, backfill queue, catalog gates and store session units; e2e for IME keys, focus outside the input and scope switching; double Shift is dispatched deterministically and the bottom-panel case waits for its known initial state; fixtures follow #869's input state.
…sh a project list that misses a reported workspace - The palette's title backfill no longer keeps a private title cache: `session.list` results go through `noteClosedChats` into the workspace's history projection (the same rows reconciliation writes), so the rename broadcast every client receives updates a chat this client never opened; a reply from an earlier connection is dropped and retried on the next schedule. - A project whose list exists but lacks a workspace the session snapshot reports (created by a peer while this client was away) is listed again, and the reply is merged with `addWorkspace` so rows that arrived meanwhile are never replaced. - `paletteBackfill.ts` holds the ordering, membership and install rules, covered with the real store: rename after backfill, missed membership, no clobbering, backfill order.
…leave Mod+Shift+I and Mod+Alt+I to the browser - A workspace's listing is keyed on the welcome generation too, so a reconnect lists it again; a timeout or a dropped connection no longer marks it listed, only a host-refused read does. - `Mod+I` toggles the preview only without Shift or Alt, so the DevTools chords pass through. - search/SPEC lists `@thinkrail/locales` among the allowed dependencies.
… reconnects, and merge listed workspaces in the host's order - A `session.list` reply is installed only if no rename landed while it was in flight (`sessionTitleRevision`, bumped on every `session_info_changed`) and the connection is the same; otherwise it is dropped and asked again. - After a reconnect, workspaces the palette listed before are listed once more, so a rename missed while away reaches their rows; the effect also re-reads when a workspace gains or loses a session. - The queue runs a key once more after it lands when it was asked for while in flight (a reply dropped across a reconnect no longer waits for the palette to reopen). - A refreshed project list keeps the host's creation order, holds on to rows that arrived meanwhile and never replaces a row the client already has. - `selectKnownChatTitle` lets an unnamed layout tab yield to the history row's title. - `createPaletteBackfill` owns the queues and listing keys with injected sources; deferred-reply tests cover a rename in flight, a reconnect, a stale reply and refusal vs timeout.
…for another trigger The backfill now watches the store itself while the palette is open (`watch()`), keyed on the welcome generation, the open projects, each workspace's session count and `sessionTitleRevision`, so a listing dropped because a rename landed in flight is asked again on its own; the hook only mounts the watch. Tests drive the store instead of calling `schedule()` by hand, including a session joining an existing workspace.
…chat in it A chat row can point at a workspace whose list was never loaded (the palette lists chats from the session snapshot), and picking it unmounts the palette, dropping its queued listings. Opening it now loads the project's list first when the row is missing, and still opens if that fails, so the location bar, the URL and the tree highlight see the workspace.
…ation, loading the list without diff stats A chat whose workspace row is missing first loads its project's list, now without diff stats and merged in the host's order (the backfill's merge), and opens only if no later pick or navigation (workspace, Project Home or tab) happened while it loaded (`paletteNavigation.ts`, unit-tested with deferred lists).
…s overtaken The overtaken check compared the active tab, which a route target from an earlier pick changes on its own when its chat finishes opening; it now compares the workspace's navigation tick, which only user navigation advances, so a legitimate pick is no longer dropped while a user tab switch still is.
…s as later picks Work in project folder entered the Default workspace without a stillWanted guard, so a late workspace.list reply could pull the user back after they chose another workspace; it now passes the navigator's pick guard. A project pick also supersedes a chat pick still loading its project, so picking the Project Home already shown is not undone by an older pick landing.
danyaberezun
force-pushed
the
global-search-research
branch
from
October 28, 2026 13:23
8e0a363 to
4806a8a
Compare
Collaborator
Author
This branch has not been deployed
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.
Problem
Finding anything in ThinkRail took a different surface for each kind of thing:
⌘Kopened a bare session switcher that only listed chats needing attention,Ctrl+Rrecalled prompts of the current chat, workspaces and projects had to be clicked through the tree, and every action — toggling an MCP server, opening a Settings section, settling a workspace — lived in the one menu or button that owned it. Nothing searched across open projects, and nothing ran a command by name.This is the first PR of the global-search stack (research and design in the originating chat: a Search-Everywhere-style palette that finds, runs and later starts work). It ships the palette foundation with no protocol change: everything it lists is already known to the client.
Redesign (Oct 26). After a second research pass over shipped palettes (IntelliJ, VS Code, Zed, Raycast, Warp, Cursor, Conductor, Codex, T3, Antigravity) the stack was cut down to what the field keeps: a finder with explicit scopes, a bounded empty state, stated ranking and previews — not a kitchen sink. This PR lost its inline Theme / Extensions / MCP pages with their page stack and crumbs, the per-row
⌘Kaction panel, in-place toggles, and theis:/has:filter grammar (commitrefactor(web): flatten the search palette to one finder level, −1 013 lines). Every setting row now opens its Settings section; a row is a destination or a command, nothing else.Rebased on
main(Oct 28, twice).maingained the shared button system (#817), first-run onboarding (#808), theask_user_questionextension (#869), repository cloning (#975) and protocol v92–v95 (#937, #869, #808, #975), so the stack's versions moved up by four: search v96 (#805), chats v97 (#887), continue v98 (#888), Find in Files v99 (#933), spec sections v100 (#939). The topbar search button is now the sharedIconButton, the palette's scope chips and Preview toggle use the sharedButton, and the palette offersmain's new Start a new project… and Open a path on the host…. The first-prompt label for unnamed chats (#911) still carries into the chat rows. The second rebase, onto7b07d5c5f(#965 web access, #975 cloning, #978/#979 onboarding fixes), had one conflict, in the store SPEC's list of Settings sections.Review (Oct 28). Two design reviews and eight rounds of code review (GPT-6 Astra, Claude Fable 5.1) changed:
⌘Kand the arrows dead. An active IME owns every key.Ctrl+Shift+R,⌘⇧Iand⌘⌥Ireach the browser.Approach
⌘K, a double tap of Shift, or the topbar search button open the palette. The empty query keeps the switcher's job as the Waiting for you group (chats that need input, failed or finished unread, across every open project, eight before "Show N more"), followed by Recent and Actions. Typing searches chats, workspaces and projects, commands and settings at once; the top hit shows only when it is clearly ahead; groups are capped with an "N more" row that narrows the scope.@work,>commands and settings; a chip writes its prefix into the query, so the prefix is the one source of scope (Tab/Alt+1…ncycle it)./files and#specs are reserved for the host search in PR2. Quoted text is a contiguous literal (no acronym or subsequence match), and an unterminated quote runs to the end. There is no filter grammar: triage happens through the empty state's grouping and the status dots every row carries.contracts, a named runtime exception in architecture Decision 3 so the host's providers rank with the same copy in PR2) decides first; within a tier a decay-free frecency (Firefox's stored form), the learned query→selection count (adaptive history), and current-project/workspace and attention boosts reorder. Learning is client-local and bounded (ids and query prefixes only, under the native adapter's cap), keyed on the text without its scope prefix and computed once per query.commandssub-module describes an invocable action (visibilitywhen,mutates, display-onlyshortcut, a requiredrun); every chord keeps its single owner and the palette calls the same handler.shellcomposes the catalog and the Settings section rows (each opens its section — the palette never flips a setting in place) and injects them;panelsimport neither new module, andsearchnever importspanels/chat/shell.⌘Kand double Shift close it, Escape clears then closes (a synthetic Escape from the desktop close-item command closes outright),Tabnever leaves the input, plainCtrl+Ris swallowed, and an active IME owns every key. Double Shift is IntelliJ's gesture (300 ms steps, reset by any other key, IME, repeat, modifiers, pointer, blur) with a per-client off switch in Settings › Keyboard.mutates) is disabled with an "Offline" reason until the current welcome, so nothing is issued blind for the transport to replay.Rejected: an all-host palette (commands and view state are browser-owned) and an all-client one (PR2's paths and transcripts cannot ship to remote clients); a fuzzy library (Fuse/cmdk's own scoring rank paths poorly); learning in host config (the conflicts the double-Shift switch solves are per machine).
Changes
packages/contracts:matchFields/parseMatchTerms/compareMatches/MATCH_TIER/MatchTerm— fixed tiers exact > prefix > word start/camel hump/acronym > substring > bounded subsequence, every term required, quoted terms literal, case and Latin accents folded without moving offsets, path-aware, field-keyed highlight ranges.apps/web/src/commands(new barrel):Command(with an optionaldisabledReason),CommandContext,isCommandVisible/isCommandEnabled/visibleCommands,shortcutKeys/shortcutLabel; its SPEC carries the coverage table of every existing action with the PR it is wired in.apps/web/src/search(new barrel): props-drivenSearchPalette(WAI-ARIA combobox/listbox,⌘Ipreview pane,Alt+1…nchips, one level — no pages), query grammar, sections/ranking/learning, client-local preferences,createDoubleShiftDetector, the Keyboard settings section.shell:SearchPaletteHost+paletteCatalog.ts+useSearchPalettereplaceSessionSwitcher/useSessionSwitcher;paletteBackfill.ts+backfillQueue.tshydrate missing workspace lists and chat titles (four requests in flight, history projection, no diff stats);useGlobalHotkeysroutesMod+Kto the palette, adds the double-Shift listeners and keepsCtrl+Rinert under it; a topbar searchIconButton.store:searchOpen+searchSession+openSearch/closeSearchand the client-localdoubleShiftSearchreplacesessionSwitcherOpen;SettingsSection.Keyboard.notifications: the aggregate attention notification opens the palette.panels:goToProjectHomeexported so the palette and the existing surfaces share one implementation;SettingsDialogtakes the injected Keyboard section.packages/locales/en.json:search.*,commands.*,settings.keyboard.*,shell.search.*; the orphanedshell.sessionSwitcher.*keys removed from every catalog (en, es, ja, zh-CN).session-switcher.spec.ts→search-palette.spec.ts(empty state, title search, commands with shortcuts, a Settings section row +⌘Kcloses, double Shift on/off, the double-Shift row → Settings › Keyboard + inertCtrl+R, IME-owned keys, keys after a click outside the input, Tab/Alt+n/chips and the preview chord).module-contracts,module-web(table + edges), newsubmodule-web-search/submodule-web-commands,submodule-web-shell,submodule-web-store,submodule-web-notifications; README gains the capability bullet.Not in this PR (the rest of the stack): host search over files, specs, plans and transcripts with the preview body (PR2), start work from the typed text (PR3), chats without a project (PR4), continue in workspace (PR5).
Screenshots
Same fixture (two chats needing attention in
sample-project), 1280×800.⌘Ksession switcher (main)⌘Kpalette, empty query (rebased build)auth>commands scopeTesting
All on the final head (
4806a8a25, the review head8e0a3631brebased ontomain7b07d5c5f) unless noted:bun run lint,check:deps,check:boundaries,check:seams,check:spec-surface,check:i18n,check:compiler→ greenbunx turbo run typecheck --filter='!@thinkrail/desktop'→ 24/24 greenapps/webbun test→ 2011 pass;bun run test(all 25 packages) → greenbun run e2e(full no-agent suite, 8 shards) → all passed on the final head, and on every review head before it (e2aaf2f2b,11f6fb767,9fa0cd0ba,e8847d5dd,4deabef3f,4515f6b10,8e0a3631b)bun run e2e e2e/search-palette.spec.ts e2e/chat-title-fallback.spec.ts --repeat-each=5→ 55 passed (review head); the new e2e cases (IME keys, keys after a click outside the input, scope keys and the preview chord) fail with their fix removedRelated issues
Supersedes the "type-to-filter command palette" deferral in #613.
Checklist
bun run lint,bun run typecheck,bun run testbun run e2e, orbun run e2e:fullwhen touching agent behavior)SPEC.md/ top-level specs updated to reflect any boundary, contract, or behavior change