feat(web): add extensible command palette#1103
feat(web): add extensible command palette#1103binbandit wants to merge 41 commits intopingdotgg:mainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| data-testid="command-palette" | ||
| > | ||
| <Command aria-label="Command palette" items={groups}> | ||
| <CommandInput placeholder="Search commands and threads..." /> |
There was a problem hiding this comment.
Is search still incoming? I don't see any code for that functionality and it doesn't appear to work on my end.
| await navigate({ | ||
| to: "/$threadId", | ||
| params: { threadId: thread.id }, |
There was a problem hiding this comment.
Not an issue with your code, but something I noticed. If this is done to a thread that is hidden beneath "view more", that section doesn't expand, so it ends up looking like no sidebar item is highlighted.
There was a problem hiding this comment.
Could you point me to how this was addressed? Still seems to be happening to me on the latest pull.
713d184 to
869181f
Compare
|
@binbandit Yours is obviously more of a strict command pallette than #1076, but it might be worth looking into. I also think that some of the ordering of actions could be slightly different and be a slightly better workflow. I have been playing with my own command pallette in a branch, and I think for me the best structure wasn't a strict Actions/Threads split, but was more like this:
I found that a lot of the "Actions" I initially had were actually pretty infrequently used, so I had to move down a lot to get to the stuff that I actually wanted. Especially stuff like "Settings", which should be very infrequently used, and can live at the very bottom of the list. Not saying this is objectively better, but it's what I found worked for me after I originally had basically the order you have. Happy to push up my branch if you wanna see what I mean. |
|
@Noojuno This is good feedback. Thanks. I was going to get this in, then allow for users to drag and drop in settings to re-organise the palette etc. Choose what they want to see etc. -- Was going to do this after so as to not bloat this pr. |
| { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, | ||
| { key: "mod+w", command: "terminal.close", when: "terminalFocus" }, | ||
| { key: "mod+d", command: "diff.toggle", when: "!terminalFocus" }, | ||
| { key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" }, |
There was a problem hiding this comment.
I think there's probably justification for triggering the command pallette even when the terminal is open. I doubt mod+k is gonna be a terminal keyboard shortcut, but I could see myself working in the terminal and wanting to navigate away and it being annoying to click out to focus the window
There was a problem hiding this comment.
Yeah true, this wasn't something i've considered, as I have previously seen the two as two different states of interaction.
- Working with the ai and the threads
- Working with the terminal and running additional commands rather than asking ai to do it.
There was a problem hiding this comment.
The main time I would run into it I think is if I'm working on two worktrees of the same thing (like T3 Code).
I will usually have my dev command running into the terminal on the worktree I was last testing, then I'll kill that command and want to navigate to the next worktree I need to check on and run the command there. Being able to Ctrl+K would be useful there (and also how I've been doing it in my command pallette branch)
| title: "New thread", | ||
| description: activeProjectTitle | ||
| ? `Create a draft thread in ${activeProjectTitle}` | ||
| : "Create a new draft thread", |
There was a problem hiding this comment.
I don't think this description adds much beyond clutter tbh. I don't think I'm getting more info than just the title, and we could compact the UI a bit by removing the descriptions that aren't needed.
If the title was just "New draft thread in ${activeProjectTitle}" I think it would be more compact overall
The desc is useful for the thread list though, with the project being the desc
| executeItem(item); | ||
| }} | ||
| > | ||
| <span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/30"> |
That's fair, although honestly I feel like that level of customisation is probs overkill tbh. It could be useful, but I don't think I can think of a command pallette in an app where I can reorder sections, or a time where I have ever really looked for that option haha. |
| const descriptionParts = [ | ||
| projectTitle, | ||
| thread.branch ? `#${thread.branch}` : null, | ||
| thread.id === activeThread?.id ? "Current thread" : null, | ||
| ].filter(Boolean); |
There was a problem hiding this comment.
If my Tray PR is ever merged, I might "steal" this as our canonical "description" for a thread. Unless we expect this to change frequently as we iterate.
| {item.description ? ( | ||
| <span className="truncate text-muted-foreground/70 text-xs"> | ||
| {item.description} | ||
| </span> |
There was a problem hiding this comment.
Do we want to truncate the description? Would it ever get too long?
| value: `thread:${thread.id}`, | ||
| label: `${thread.title} ${projectTitle ?? ""} ${thread.branch ?? ""}`.trim(), | ||
| title: thread.title, | ||
| description: descriptionParts.join(" · "), |
There was a problem hiding this comment.
Using this character (or really any separator character) might not be super friendly to screen readers. Perhaps that could be fixed with ARIA.
|
@huxcrux ooft quality suggestions. Thank you! U wanna contribute them to own them? Reach out on discord and ill give you write access so then your name appears next to the feature 😄 |
|
Heyyy, nice work! We can close 1076 if you consider adding add project functionality with autocompletion here, I mean we can close it regardless 🤣 but I would really love that feature. |
do you want to contribute that feature of it? So then your work / recognition doesn't get lost? |
Would love to! The filesystem browse endpoint and autocomplete logic from #1076 should fit right in, commits in there are detailed so we could cherry pick the relevant pieces. I'll reach out on Discord. edit: @binbandit invite sent. |
| readonly items: ReadonlyArray<CommandPaletteItem>; | ||
| } | ||
|
|
||
| const CommandPaletteContext = createContext<CommandPaletteState | null>(null); |
There was a problem hiding this comment.
Any thoughts on moving this out into zustand to match (most of) the rest of the app state?
Unify label and searchFields into a single searchTerms array for clearer semantics. Extract filterBrowseEntries and buildRootGroups as pure functions. Remove nested ternaries and fix timestamp test format.
Remove projectAdd.ts abstraction, inline add-project logic in both Sidebar and CommandPalette. Extract sortThreads and getLatestThreadForProject into lib/threadSort.ts for shared use. Use server-expanded browse paths for duplicate detection. Fix browse mode Enter handling and existing project navigation.
bf38df8 to
d836174
Compare
…mand-palette # Conflicts: # apps/web/src/components/ChatView.browser.tsx # apps/web/src/components/Sidebar.tsx
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
…-core # Conflicts: # apps/web/src/components/Sidebar.logic.test.ts # apps/web/src/components/Sidebar.logic.ts
|
another banggggerrrr |
…mand-palette # Conflicts: # apps/web/src/components/Sidebar.logic.ts # apps/web/src/components/Sidebar.tsx # apps/web/src/routes/_chat.tsx
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| sortOrder, | ||
| )[0] ?? null | ||
| ); | ||
| } |
There was a problem hiding this comment.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
|
|
||
| const drain: DrainableWorker<A>["drain"] = Ref.get(state).pipe( | ||
| Effect.flatMap(({ idle }) => Deferred.await(idle)), | ||
| ); |
There was a problem hiding this comment.
DrainableWorker drain can resolve prematurely under concurrency
Medium Severity
The drain implementation uses a non-atomic Ref.get followed by Deferred.await. Between these two operations, another fiber can call enqueue, replacing the idle deferred in state. Since drain captured the old (already-resolved) deferred from Ref.get, Deferred.await resolves immediately even though the newly enqueued item is still processing. The previous STM-based implementation (TxRef.get + Effect.txRetry inside Effect.tx) was atomic and would retry if outstanding changed during the transaction, preventing this race.
Additional Locations (1)
| return Number.isFinite(ms) ? ms : null; | ||
| } | ||
|
|
||
| function getLatestUserMessageTimestamp(thread: SidebarThreadSortInput): number { |
There was a problem hiding this comment.
Duplicated toSortableTimestamp after thread sort extraction
Low Severity
The toSortableTimestamp helper was copied into threadSort.ts during the extraction but the original private copy in Sidebar.logic.ts was left in place. Both are identical. Since Sidebar.logic.ts already imports from threadSort.ts, the local copy is redundant and could diverge if only one is updated in the future.
Additional Locations (1)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| return formatUnit(Math.floor(diffMs / MONTH_MS), "month", "mo"); | ||
| } | ||
| return formatUnit(Math.floor(diffMs / YEAR_MS), "year", "y"); | ||
| } |
There was a problem hiding this comment.
Duplicate relative time formatting utility with same name
Low Severity
A new formatRelativeTime function is introduced in relativeTime.ts while timestampFormat.ts already exports a function with the same name. Both format relative timestamps from ISO dates but with different signatures and capabilities. Having two independent utilities sharing the same exported name increases confusion risk — a developer searching for relative time formatting may pick the wrong one. The existing formatRelativeTime in timestampFormat.ts could be extended (or the new one could reuse its core logic) rather than maintaining two parallel implementations.





Summary
commandPalette.togglethrough contracts, server defaults, the sidebar trigger, and keybinding docsValidation
bun fmtbun lintbun typecheckbun run test -- src/keybindings.test.tsinpackages/contractsbun run test -- src/keybindings.test.tsinapps/serverbun run test -- src/keybindings.test.tsinapps/webbun run test:browser -- src/components/ChatView.browser.tsx -t "opens the command palette from the configurable shortcut and runs a command"inapps/webVisual
Note
Add extensible command palette with mod+k shortcut to the sidebar layout
mod+kor a sidebar trigger) that supports actions, project navigation, thread search, and submenus with keyboard hints.CommandPalette.logic.ts, thread sorting utilities inthreadSort.ts, and open/close state via a new Zustand store incommandPaletteStore.ts._chat.tsxnow toggle the palette oncommandPalette.toggleand suppress all other chat shortcuts while the palette is open.formatRelativeTimefor human-readable timestamps shown in thread results, and centralizes new-thread creation logic intostartNewThreadFromContext/startNewLocalThreadFromContexthelpers.ChatViewglobal keydown handler exits early when the command palette is open, so shortcuts likechat.neware blocked while the palette is visible.Macroscope summarized 8e22d9b.
Note
Medium Risk
Adds a new globally-invoked command palette and rewires keybinding handling for thread creation and shortcut dispatch; main risk is UX/regression around keyboard shortcuts and navigation while the palette is open.
Overview
Introduces a layout-scoped command palette in the web app, opened via a new sidebar search trigger and a new configurable
commandPalette.togglekeybinding (defaultmod+kwhen!terminalFocus). The palette supports searching across actions, projects, and threads, launching navigation and thread-creation flows, with ranking/filters (including excluding archived threads) and relative timestamps.Keybinding plumbing is extended end-to-end (contracts, server defaults, docs, and client keybinding tests), and global shortcut handling is adjusted to toggle/suppress shortcuts while the palette is open. Thread creation logic is centralized in
chatThreadActions, and thread sorting is extracted intolib/threadSort(used by sidebar + palette) with new tests.Also refactors
DrainableWorkeroff STM primitives toQueue/Ref/Deferred, and tweaks overlay z-index/backdrop styling to ensure toasts/dialogs stack correctly.Written by Cursor Bugbot for commit 8e22d9b. This will update automatically on new commits. Configure here.