Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
assert.equal(defaultsByCommand.get("thread.previous"), "mod+shift+[");
assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]");
assert.equal(defaultsByCommand.get("thread.settle"), "mod+shift+s");
assert.equal(defaultsByCommand.get("thread.pin"), "mod+shift+p");
assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9");
assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m");
Expand Down
27 changes: 26 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,7 @@ function ChatViewContent(props: ChatViewProps) {
const threadSyncPhase = routeKind === "server" ? (props.threadSyncPhase ?? null) : null;
const threadDetailLoading = threadSyncPhase === "loading";
const handleNewThread = useNewThreadHandler();
const { settleThread } = useThreadActions();
const { settleThread, pinThread, unpinThread } = useThreadActions();
const routeThreadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
[environmentId, threadId],
Expand Down Expand Up @@ -4428,6 +4428,8 @@ function ChatViewContent(props: ChatViewProps) {
);
const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true;
const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true;
const supportsPinning = serverConfig?.environment.capabilities.threadPinning === true;
const activeThreadPinned = supportsPinning && activeThreadShell?.pinnedAt != null;
const nowMinute = useNowMinute();
const snoozeNow = new Date().toISOString();
const activeThreadSnoozed =
Expand Down Expand Up @@ -5140,6 +5142,25 @@ function ChatViewContent(props: ChatViewProps) {
return;
}

if (command === "thread.pin") {
event.preventDefault();
event.stopPropagation();
if (!isServerThread || !activeThreadRef || !supportsPinning) return;
const pinned = activeThreadPinned;
void (pinned ? unpinThread(activeThreadRef) : pinThread(activeThreadRef)).then((result) => {
if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return;
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: pinned ? "Failed to unpin thread" : "Failed to pin thread",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
});
return;
}

if (command === "terminal.toggle") {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -5244,6 +5265,7 @@ function ChatViewContent(props: ChatViewProps) {
activeRightPanelSurface,
addTerminalSurface,
activeThreadRef,
activeThreadPinned,
activeThreadSettled,
terminalUiState.terminalOpen,
terminalUiState.activeTerminalId,
Expand All @@ -5259,8 +5281,11 @@ function ChatViewContent(props: ChatViewProps) {
handleUnsettleActiveThread,
isServerThread,
onToggleDiff,
pinThread,
settleThread,
supportsPinning,
supportsSettlement,
unpinThread,
toggleRightPanel,
toggleRightPanelMaximized,
toggleTerminalVisibility,
Expand Down
4 changes: 4 additions & 0 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ so add one in **Settings** → **Keybindings** if you want to use it.
`thread.settle` settles the active thread or restores it when it is already settled. Its default
shortcut is `mod+shift+s`, and it does not run while the terminal has focus.

`thread.pin` pins the active thread to the pinned section of the sidebar, or unpins it when it is
already pinned. Its default shortcut is `mod+shift+p`, and it does not run while the terminal has
focus. See [Organizing threads](./thread-sidebar.md) for how pinned threads are ordered.

The command palette searches active thread titles, projects, branches, user messages, and final
agent responses across connected environments. Message matches show one labeled excerpt while
keeping the thread's project, branch, and machine context visible. Message search begins after two
Expand Down
4 changes: 2 additions & 2 deletions docs/user/thread-sidebar.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Organizing threads

Pin a thread from its context menu to keep it in the pinned section above your active work.
Pinned threads are shown independently of their project, including when you connect to more than
one environment.
`mod+shift+p` pins or unpins the thread you have open. Pinned threads are shown independently of
their project, including when you connect to more than one environment.

Pinned threads still move to **Settled** when they become inactive. They also move when their pull
request merges if **Auto-settle merged threads** is enabled.
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const THREAD_KEYBINDING_COMMANDS = [
"thread.previous",
"thread.next",
"thread.settle",
"thread.pin",
...THREAD_JUMP_KEYBINDING_COMMANDS,
] as const;
export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number];
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ key: "mod+shift+[", command: "thread.previous" },
{ key: "mod+shift+]", command: "thread.next" },
{ key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" },
{ key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" },
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
command,
Expand Down
Loading