diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 9af410f5722e..0c1abcb423b1 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1584,6 +1584,7 @@ export default function ChatView(props: ChatViewProps) { }; }, [routeKind, routeThreadRef, routeThreadState]); const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); + const markThreadUnread = useUiStateStore((store) => store.markThreadUnread); const settings = useEnvironmentSettings(environmentId); const setStickyComposerModelSelection = useComposerDraftStore( (store) => store.setStickyModelSelection, @@ -6761,6 +6762,15 @@ export default function ChatView(props: ChatViewProps) { return; } + if (command === "thread.markUnread") { + const completedAt = activeLatestTurn?.completedAt; + if (!isServerThread || !activeThreadKey || !completedAt) return; + event.preventDefault(); + event.stopPropagation(); + markThreadUnread(activeThreadKey, completedAt); + return; + } + if (command === "terminal.toggle") { event.preventDefault(); event.stopPropagation(); @@ -6925,6 +6935,8 @@ export default function ChatView(props: ChatViewProps) { activeThreadRef, activeThreadPinned, activeThreadSettled, + activeLatestTurn?.completedAt, + markThreadUnread, canInterruptRunningThread, activeThreadKey, terminalUiState.terminalOpen, diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts index 10fc154d5b0c..f53a5b0338b3 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts @@ -243,12 +243,16 @@ describe("KeybindingsSettings.logic", () => { "chat.new", "rightPanel.toggleMaximized", "thread.stop", + "thread.markUnread", "script.setup-db.run", ]), ); expect(DEFAULT_RESOLVED_KEYBINDINGS.some((binding) => binding.command === "thread.stop")).toBe( false, ); + expect( + DEFAULT_RESOLVED_KEYBINDINGS.some((binding) => binding.command === "thread.markUnread"), + ).toBe(false); }); it("reports unknown when variables without rejecting parseable expressions", () => { diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index 2d9da2e5ebc1..a9eb3224842a 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -832,16 +832,19 @@ describe("cross-command precedence", () => { }); describe("resolveShortcutCommand", () => { - it("resolves a custom stop-thread shortcut", () => { - const keybindings = compile([{ shortcut: modShortcut("escape"), command: "thread.stop" }]); + it.each(["thread.stop", "thread.markUnread"] as const)( + "resolves a custom %s shortcut", + (command) => { + const keybindings = compile([{ shortcut: modShortcut("escape"), command }]); - assert.strictEqual( - resolveShortcutCommand(event({ key: "Escape", metaKey: true }), keybindings, { - platform: "MacIntel", - }), - "thread.stop", - ); - }); + assert.strictEqual( + resolveShortcutCommand(event({ key: "Escape", metaKey: true }), keybindings, { + platform: "MacIntel", + }), + command, + ); + }, + ); it("honors preview conditions for a stop-thread shortcut", () => { const keybindings = compile([ diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index ad1b157bbb75..6d3f329836ea 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -37,6 +37,13 @@ to copy its URL and `mod+shift+k` to copy its number with a `#` prefix. Both shortcuts can be changed in Settings. Search for “Copy Link or Thread ID” or “Copy Number”. They copy the selected PR and leave terminal input alone. +## Mark a thread unread + +`thread.markUnread` marks the active thread unread after a completed turn. It has +no default shortcut; assign one in **Settings → Keybindings** on web and desktop. +Add `!terminalFocus` as its condition to leave terminal input alone. +The thread stays unread until you reopen it or a new turn completes. + ## iPad With a hardware keyboard, use `Cmd+1` through `Cmd+9` to open the first nine diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 3d3215a193f6..df0e816a49db 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -120,6 +120,10 @@ The linked pull request participates in automatic settlement. ## Find and reference work +Choose **Mark unread** from a thread's menu to revisit a completed response later. +On web and desktop, assign a shortcut to `thread.markUnread` in **Settings → Keybindings** +to mark the open thread unread. Reopening the thread marks it read again. + On web and desktop, open the command palette with `Cmd/Ctrl+K` to search threads across connected environments. Message search starts after two characters and includes your messages and final agent responses. diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 75dc89195969..bf12dc03c701 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -43,6 +43,7 @@ const THREAD_KEYBINDING_COMMANDS = [ "thread.settle", "thread.pin", "thread.undo", + "thread.markUnread", ...THREAD_JUMP_KEYBINDING_COMMANDS, ] as const; export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number];