| 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,6 +196,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 196 | assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+["); | 196 | assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+["); |
| 197 | assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]"); | 197 | assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]"); |
| 198 | assert.equal(defaultsByCommand.get("thread.settle"), "mod+shift+s"); | 198 | assert.equal(defaultsByCommand.get("thread.settle"), "mod+shift+s"); |
| 199 | assert.equal(defaultsByCommand.get("thread.pin"), "mod+shift+p"); | ||
| 199 | assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1"); | 200 | assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1"); |
| 200 | assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9"); | 201 | assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9"); |
| 201 | assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m"); | 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,7 +1273,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1273 | const threadSyncPhase = routeKind === "server" ? (props.threadSyncPhase ?? null) : null; | 1273 | const threadSyncPhase = routeKind === "server" ? (props.threadSyncPhase ?? null) : null; |
| 1274 | const threadDetailLoading = threadSyncPhase === "loading"; | 1274 | const threadDetailLoading = threadSyncPhase === "loading"; |
| 1275 | const handleNewThread = useNewThreadHandler(); | 1275 | const handleNewThread = useNewThreadHandler(); |
| 1276 | const { settleThread } = useThreadActions(); | 1276 | const { settleThread, pinThread, unpinThread } = useThreadActions(); |
| 1277 | const routeThreadRef = useMemo( | 1277 | const routeThreadRef = useMemo( |
| 1278 | () => scopeThreadRef(environmentId, threadId), | 1278 | () => scopeThreadRef(environmentId, threadId), |
| 1279 | [environmentId, threadId], | 1279 | [environmentId, threadId], |
| ... | @@ -4429,6 +4429,8 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4429,6 +4429,8 @@ function ChatViewContent(props: ChatViewProps) { |
| 4429 | ); | 4429 | ); |
| 4430 | const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; | 4430 | const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; |
| 4431 | const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true; | 4431 | const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true; |
| 4432 | const supportsPinning = serverConfig?.environment.capabilities.threadPinning === true; | ||
| 4433 | const activeThreadPinned = supportsPinning && activeThreadShell?.pinnedAt != null; | ||
| 4432 | const nowMinute = useNowMinute(); | 4434 | const nowMinute = useNowMinute(); |
| 4433 | const snoozeNow = new Date().toISOString(); | 4435 | const snoozeNow = new Date().toISOString(); |
| 4434 | const activeThreadSnoozed = | 4436 | const activeThreadSnoozed = |
| ... | @@ -5141,6 +5143,25 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5141,6 +5143,25 @@ function ChatViewContent(props: ChatViewProps) { |
| 5141 | return; | 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 | if (command === "terminal.toggle") { | 5165 | if (command === "terminal.toggle") { |
| 5145 | event.preventDefault(); | 5166 | event.preventDefault(); |
| 5146 | event.stopPropagation(); | 5167 | event.stopPropagation(); |
| ... | @@ -5245,6 +5266,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5245,6 +5266,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5245 | activeRightPanelSurface, | 5266 | activeRightPanelSurface, |
| 5246 | addTerminalSurface, | 5267 | addTerminalSurface, |
| 5247 | activeThreadRef, | 5268 | activeThreadRef, |
| 5269 | activeThreadPinned, | ||
| 5248 | activeThreadSettled, | 5270 | activeThreadSettled, |
| 5249 | terminalUiState.terminalOpen, | 5271 | terminalUiState.terminalOpen, |
| 5250 | terminalUiState.activeTerminalId, | 5272 | terminalUiState.activeTerminalId, |
| ... | @@ -5260,8 +5282,11 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5260,8 +5282,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 5260 | handleUnsettleActiveThread, | 5282 | handleUnsettleActiveThread, |
| 5261 | isServerThread, | 5283 | isServerThread, |
| 5262 | onToggleDiff, | 5284 | onToggleDiff, |
| 5285 | pinThread, | ||
| 5263 | settleThread, | 5286 | settleThread, |
| 5287 | supportsPinning, | ||
| 5264 | supportsSettlement, | 5288 | supportsSettlement, |
| 5289 | unpinThread, | ||
| 5265 | toggleRightPanel, | 5290 | toggleRightPanel, |
| 5266 | toggleRightPanelMaximized, | 5291 | toggleRightPanelMaximized, |
| 5267 | toggleTerminalVisibility, | 5292 | toggleTerminalVisibility, |
docs/user/keybindings.md+4| ... | @@ -55,6 +55,10 @@ so add one in **Settings** → **Keybindings** if you want to use it. | ... | @@ -55,6 +55,10 @@ so add one in **Settings** → **Keybindings** if you want to use it. |
| 55 | `thread.settle` settles the active thread or restores it when it is already settled. Its default | 55 | `thread.settle` settles the active thread or restores it when it is already settled. Its default |
| 56 | shortcut is `mod+shift+s`, and it does not run while the terminal has focus. | 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 | The command palette searches active thread titles, projects, branches, user messages, and final | 62 | The command palette searches active thread titles, projects, branches, user messages, and final |
| 59 | agent responses across connected environments. Message matches show one labeled excerpt while | 63 | agent responses across connected environments. Message matches show one labeled excerpt while |
| 60 | keeping the thread's project, branch, and machine context visible. Message search begins after two | 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,8 +1,8 @@ |
| 1 | # Organizing threads | 1 | # Organizing threads |
| 2 | 2 | ||
| 3 | Pin a thread from its context menu to keep it in the pinned section above your active work. | 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 | 4 | `mod+shift+p` pins or unpins the thread you have open. Pinned threads are shown independently of |
| 5 | one environment. | 5 | their project, including when you connect to more than one environment. |
| 6 | 6 | ||
| 7 | Pinned threads still move to **Settled** when they become inactive. They also move when their pull | 7 | Pinned threads still move to **Settled** when they become inactive. They also move when their pull |
| 8 | request merges if **Auto-settle merged threads** is enabled. | 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,6 +38,7 @@ export const THREAD_KEYBINDING_COMMANDS = [ |
| 38 | "thread.previous", | 38 | "thread.previous", |
| 39 | "thread.next", | 39 | "thread.next", |
| 40 | "thread.settle", | 40 | "thread.settle", |
| 41 | "thread.pin", | ||
| 41 | ...THREAD_JUMP_KEYBINDING_COMMANDS, | 42 | ...THREAD_JUMP_KEYBINDING_COMMANDS, |
| 42 | ] as const; | 43 | ] as const; |
| 43 | export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number]; | 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,6 +47,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 47 | { key: "mod+shift+[", command: "thread.previous" }, | 47 | { key: "mod+shift+[", command: "thread.previous" }, |
| 48 | { key: "mod+shift+]", command: "thread.next" }, | 48 | { key: "mod+shift+]", command: "thread.next" }, |
| 49 | { key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" }, | 49 | { key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" }, |
| 50 | { key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" }, | ||
| 50 | ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ | 51 | ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ |
| 51 | key: `mod+${index + 1}`, | 52 | key: `mod+${index + 1}`, |
| 52 | command, | 53 | command, |