| author | |
| committer | |
| log | 4c51b4c9b6a85d96a22e0df41d5cfd2d8fc9901d |
| tree | 319728ae4d8df35d037d85532b0e256c56795da7 |
| parent | 8f49132214a40c85cf46bf5e3d8ea11c04a9610a |
| signature | Signed by PGP key B5690EEEBB952194 |
Add a mod+shift+p shortcut that pins or unpins the active thread. Ignore the shortcut during terminal focus and when the server does not support thread pinning.6 files changed, 35 insertions(+), 3 deletions(-)
apps/server/src/keybindings.test.ts+1| ... | ... | @@ -196,6 +196,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 196 | 196 | assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+["); |
| 197 | 197 | assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]"); |
| 198 | 198 | assert.equal(defaultsByCommand.get("thread.settle"), "mod+shift+s"); |
| 199 | assert.equal(defaultsByCommand.get("thread.pin"), "mod+shift+p"); | |
| 199 | 200 | assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1"); |
| 200 | 201 | assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9"); |
| 201 | 202 | assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m"); |
apps/web/src/components/ChatView.tsx+26-1| ... | ... | @@ -1273,7 +1273,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1273 | 1273 | const threadSyncPhase = routeKind === "server" ? (props.threadSyncPhase ?? null) : null; |
| 1274 | 1274 | const threadDetailLoading = threadSyncPhase === "loading"; |
| 1275 | 1275 | const handleNewThread = useNewThreadHandler(); |
| 1276 | const { settleThread } = useThreadActions(); | |
| 1276 | const { settleThread, pinThread, unpinThread } = useThreadActions(); | |
| 1277 | 1277 | const routeThreadRef = useMemo( |
| 1278 | 1278 | () => scopeThreadRef(environmentId, threadId), |
| 1279 | 1279 | [environmentId, threadId], |
| ... | ... | @@ -4429,6 +4429,8 @@ function ChatViewContent(props: ChatViewProps) { |
| 4429 | 4429 | ); |
| 4430 | 4430 | const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; |
| 4431 | 4431 | const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true; |
| 4432 | const supportsPinning = serverConfig?.environment.capabilities.threadPinning === true; | |
| 4433 | const activeThreadPinned = supportsPinning && activeThreadShell?.pinnedAt != null; | |
| 4432 | 4434 | const nowMinute = useNowMinute(); |
| 4433 | 4435 | const snoozeNow = new Date().toISOString(); |
| 4434 | 4436 | const activeThreadSnoozed = |
| ... | ... | @@ -5141,6 +5143,25 @@ function ChatViewContent(props: ChatViewProps) { |
| 5141 | 5143 | return; |
| 5142 | 5144 | } |
| 5143 | 5145 | |
| 5146 | if (command === "thread.pin") { | |
| 5147 | event.preventDefault(); | |
| 5148 | event.stopPropagation(); | |
| 5149 | if (!isServerThread || !activeThreadRef || !supportsPinning) return; | |
| 5150 | const pinned = activeThreadPinned; | |
| 5151 | void (pinned ? unpinThread(activeThreadRef) : pinThread(activeThreadRef)).then((result) => { | |
| 5152 | if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; | |
| 5153 | const error = squashAtomCommandFailure(result); | |
| 5154 | toastManager.add( | |
| 5155 | stackedThreadToast({ | |
| 5156 | type: "error", | |
| 5157 | title: pinned ? "Failed to unpin thread" : "Failed to pin thread", | |
| 5158 | description: error instanceof Error ? error.message : "An error occurred.", | |
| 5159 | }), | |
| 5160 | ); | |
| 5161 | }); | |
| 5162 | return; | |
| 5163 | } | |
| 5164 | ||
| 5144 | 5165 | if (command === "terminal.toggle") { |
| 5145 | 5166 | event.preventDefault(); |
| 5146 | 5167 | event.stopPropagation(); |
| ... | ... | @@ -5245,6 +5266,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5245 | 5266 | activeRightPanelSurface, |
| 5246 | 5267 | addTerminalSurface, |
| 5247 | 5268 | activeThreadRef, |
| 5269 | activeThreadPinned, | |
| 5248 | 5270 | activeThreadSettled, |
| 5249 | 5271 | terminalUiState.terminalOpen, |
| 5250 | 5272 | terminalUiState.activeTerminalId, |
| ... | ... | @@ -5260,8 +5282,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 5260 | 5282 | handleUnsettleActiveThread, |
| 5261 | 5283 | isServerThread, |
| 5262 | 5284 | onToggleDiff, |
| 5285 | pinThread, | |
| 5263 | 5286 | settleThread, |
| 5287 | supportsPinning, | |
| 5264 | 5288 | supportsSettlement, |
| 5289 | unpinThread, | |
| 5265 | 5290 | toggleRightPanel, |
| 5266 | 5291 | toggleRightPanelMaximized, |
| 5267 | 5292 | toggleTerminalVisibility, |
docs/user/keybindings.md+4| ... | ... | @@ -55,6 +55,10 @@ so add one in **Settings** → **Keybindings** if you want to use it. |
| 55 | 55 | `thread.settle` settles the active thread or restores it when it is already settled. Its default |
| 56 | 56 | shortcut is `mod+shift+s`, and it does not run while the terminal has focus. |
| 57 | 57 | |
| 58 | `thread.pin` pins the active thread to the pinned section of the sidebar, or unpins it when it is | |
| 59 | already pinned. Its default shortcut is `mod+shift+p`, and it does not run while the terminal has | |
| 60 | focus. See [Organizing threads](./thread-sidebar.md) for how pinned threads are ordered. | |
| 61 | ||
| 58 | 62 | The command palette searches active thread titles, projects, branches, user messages, and final |
| 59 | 63 | agent responses across connected environments. Message matches show one labeled excerpt while |
| 60 | 64 | keeping the thread's project, branch, and machine context visible. Message search begins after two |
docs/user/thread-sidebar.md+2-2| ... | ... | @@ -1,8 +1,8 @@ |
| 1 | 1 | # Organizing threads |
| 2 | 2 | |
| 3 | 3 | Pin a thread from its context menu to keep it in the pinned section above your active work. |
| 4 | Pinned threads are shown independently of their project, including when you connect to more than | |
| 5 | one environment. | |
| 4 | `mod+shift+p` pins or unpins the thread you have open. Pinned threads are shown independently of | |
| 5 | their project, including when you connect to more than one environment. | |
| 6 | 6 | |
| 7 | 7 | Pinned threads still move to **Settled** when they become inactive. They also move when their pull |
| 8 | 8 | request merges if **Auto-settle merged threads** is enabled. |
packages/contracts/src/keybindings.ts+1| ... | ... | @@ -38,6 +38,7 @@ export const THREAD_KEYBINDING_COMMANDS = [ |
| 38 | 38 | "thread.previous", |
| 39 | 39 | "thread.next", |
| 40 | 40 | "thread.settle", |
| 41 | "thread.pin", | |
| 41 | 42 | ...THREAD_JUMP_KEYBINDING_COMMANDS, |
| 42 | 43 | ] as const; |
| 43 | 44 | export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number]; |
packages/shared/src/keybindings.ts+1| ... | ... | @@ -47,6 +47,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 47 | 47 | { key: "mod+shift+[", command: "thread.previous" }, |
| 48 | 48 | { key: "mod+shift+]", command: "thread.next" }, |
| 49 | 49 | { key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" }, |
| 50 | { key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" }, | |
| 50 | 51 | ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ |
| 51 | 52 | key: `mod+${index + 1}`, |
| 52 | 53 | command, |