From 50b50373c9aaf7a4a42fb22ea652d3ab1d9f9bc1 Mon Sep 17 00:00:00 2001 From: id0sch Date: Tue, 15 Sep 2026 15:06:04 +0000 Subject: [PATCH 1/4] feat(web): mark the active thread unread from the keyboard --- apps/web/src/components/ChatView.tsx | 11 +++++++++++ apps/web/src/keybindings.test.ts | 1 + docs/user/keybindings.md | 9 +++++++++ docs/user/thread-sidebar.md | 5 +++++ packages/contracts/src/keybindings.ts | 1 + packages/shared/src/keybindings.ts | 1 + 6 files changed, 28 insertions(+) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 38f773cf037e..39c091e7bebf 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1562,6 +1562,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, @@ -6656,6 +6657,14 @@ export default function ChatView(props: ChatViewProps) { return; } + if (command === "thread.markUnread") { + event.preventDefault(); + event.stopPropagation(); + if (!isServerThread || !activeThreadKey) return; + markThreadUnread(activeThreadKey, activeLatestTurn?.completedAt); + return; + } + if (command === "terminal.toggle") { event.preventDefault(); event.stopPropagation(); @@ -6820,6 +6829,8 @@ export default function ChatView(props: ChatViewProps) { activeThreadRef, activeThreadPinned, activeThreadSettled, + activeLatestTurn?.completedAt, + markThreadUnread, canInterruptRunningThread, activeThreadKey, terminalUiState.terminalOpen, diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index d335c68869bb..992dfee0f3f4 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -1120,6 +1120,7 @@ describe("composer and pull request shortcuts", () => { ["c", "thread.copyReference"], ["k", "pullRequest.copyNumber"], ["Enter", "thread.steerQueuedMessage"], + ["u", "thread.markUnread"], ] as const; for (const platform of ["MacIntel", "Win32", "Linux"]) { diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index acf6bd2aa1ac..81714621a260 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -28,6 +28,15 @@ 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 with `mod+shift+u` on web and +desktop. It requires a completed turn and does not run while the terminal has focus. +The thread stays unread until you reopen it or a new turn completes. + +Microsoft Edge uses this shortcut for Read Aloud, and some Linux input methods use +`Ctrl+Shift+U` for Unicode entry. Rebind it in **Settings → Keybindings** if needed. + ## 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 67574f0b0286..0d109d7e9571 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -109,6 +109,11 @@ 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, `mod+shift+u` marks the thread you have open unread; reopening +the thread marks it read again. See [keybindings](./keybindings.md#mark-a-thread-unread) +to customize the shortcut. + 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 1256dcc77786..ab33d538e951 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -42,6 +42,7 @@ const THREAD_KEYBINDING_COMMANDS = [ "thread.copyReference", "thread.settle", "thread.pin", + "thread.markUnread", ...THREAD_JUMP_KEYBINDING_COMMANDS, ] as const; export type ThreadKeybindingCommand = (typeof THREAD_KEYBINDING_COMMANDS)[number]; diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 0cc18680e6d5..89850edabe01 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -60,6 +60,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+shift+c", command: "thread.copyReference", when: "!terminalFocus" }, { key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" }, { key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" }, + { key: "mod+shift+u", command: "thread.markUnread", when: "!terminalFocus" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, From bd783b716788bee1c1f77c46399edb82e6cfb483 Mon Sep 17 00:00:00 2001 From: id0sch Date: Thu, 17 Sep 2026 05:34:13 +0000 Subject: [PATCH 2/4] fix(web): leave mark unread shortcut unbound by default --- .../KeybindingsSettings.logic.test.ts | 4 ++++ apps/web/src/keybindings.test.ts | 22 ++++++++++--------- docs/user/keybindings.md | 8 +++---- docs/user/thread-sidebar.md | 5 ++--- packages/shared/src/keybindings.ts | 1 - 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts index ff3b30124a73..5a96f49279bc 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts @@ -235,12 +235,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 992dfee0f3f4..ef9b48ed4822 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -743,16 +743,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([ @@ -1120,7 +1123,6 @@ describe("composer and pull request shortcuts", () => { ["c", "thread.copyReference"], ["k", "pullRequest.copyNumber"], ["Enter", "thread.steerQueuedMessage"], - ["u", "thread.markUnread"], ] as const; for (const platform of ["MacIntel", "Win32", "Linux"]) { diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 81714621a260..3e8bb89ff7b2 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -30,13 +30,11 @@ or “Copy Number”. They copy the selected PR and leave terminal input alone. ## Mark a thread unread -`thread.markUnread` marks the active thread unread with `mod+shift+u` on web and -desktop. It requires a completed turn and does not run while the terminal has focus. +`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. -Microsoft Edge uses this shortcut for Read Aloud, and some Linux input methods use -`Ctrl+Shift+U` for Unicode entry. Rebind it in **Settings → Keybindings** if needed. - ## 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 0d109d7e9571..fc7a0e9f5f04 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -110,9 +110,8 @@ 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, `mod+shift+u` marks the thread you have open unread; reopening -the thread marks it read again. See [keybindings](./keybindings.md#mark-a-thread-unread) -to customize the shortcut. +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 diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 89850edabe01..0cc18680e6d5 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -60,7 +60,6 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+shift+c", command: "thread.copyReference", when: "!terminalFocus" }, { key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" }, { key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" }, - { key: "mod+shift+u", command: "thread.markUnread", when: "!terminalFocus" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, From c1b8fae69875740619f81dc21d14be6111e2cdfe Mon Sep 17 00:00:00 2001 From: id0sch Date: Thu, 17 Sep 2026 06:18:54 +0000 Subject: [PATCH 3/4] test(web): cover mark unread shortcut handler --- .../web/src/components/ChatView.logic.test.ts | 44 ++++++++++++++++++- apps/web/src/components/ChatView.logic.ts | 14 +++++- apps/web/src/components/ChatView.tsx | 12 ++--- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index c226a3a65c78..f4bfe86f7ea1 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -16,10 +16,11 @@ import { Atom, AsyncResult } from "effect/unstable/reactivity"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentThreadDetails } from "../state/threads"; +import { useUiStateStore } from "../uiStateStore"; import type { Thread, ThreadShell, TurnDiffSummary } from "../types"; import { deriveProviderInstanceEntries, NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates"; -import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { type RightPanelSurface, pullRequestSurface, @@ -34,6 +35,7 @@ import { MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, agentControlledBrowserCloseConfirmation, + handleMarkThreadUnreadShortcut, branchMismatchKey, buildExpiredTerminalContextToastCopy, buildLoadingThreadFromShell, @@ -2619,3 +2621,43 @@ describe("worktree setup visibility", () => { ).toEqual(base); }); }); + +describe("mark unread shortcut", () => { + const threadRef = scopeThreadRef(EnvironmentId.make("env-1"), ThreadId.make("thread-1")); + const threadKey = scopedThreadKey(threadRef); + const otherKey = scopedThreadKey({ ...threadRef, environmentId: EnvironmentId.make("env-2") }); + const completedAt = "2026-02-25T12:30:00.000Z"; + + beforeEach(() => { + useUiStateStore.setState({ + threadLastVisitedAtById: { [threadKey]: completedAt, [otherKey]: completedAt }, + }); + }); + afterEach(() => useUiStateStore.setState({ threadLastVisitedAtById: {} })); + + it("marks only the scoped active thread unread and consumes the key event", () => { + const event = new Event("keydown", { cancelable: true, bubbles: true }); + handleMarkThreadUnreadShortcut(event, threadRef, completedAt); + + expect(event.defaultPrevented).toBe(true); + expect(event.cancelBubble).toBe(true); + expect(useUiStateStore.getState().threadLastVisitedAtById).toEqual({ + [threadKey]: "2026-02-25T12:29:59.999Z", + [otherKey]: completedAt, + }); + + const unreadState = useUiStateStore.getState(); + handleMarkThreadUnreadShortcut(event, threadRef, completedAt); + expect(useUiStateStore.getState()).toBe(unreadState); + }); + + it.each([ + { ref: null, completion: completedAt }, + { ref: threadRef, completion: undefined }, + { ref: threadRef, completion: null }, + ])("leaves visit state unchanged for $ref / $completion", ({ ref, completion }) => { + const state = useUiStateStore.getState(); + handleMarkThreadUnreadShortcut(new Event("keydown"), ref, completion); + expect(useUiStateStore.getState()).toBe(state); + }); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 1dbe11f6c4c2..91753f6c0670 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -21,7 +21,7 @@ import { WORKTREE_SETUP_ACTIVITY_KIND, WorktreeSetupSnapshot, } from "@t3tools/contracts"; -import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; +import { parseScopedThreadKey, scopedThreadKey } from "@t3tools/client-runtime/environment"; import { resolveAssetUrl } from "@t3tools/client-runtime/state/assets"; import { squashAtomCommandFailure, @@ -60,6 +60,18 @@ import { resolveSelectableProviderInstanceEntry, type ProviderInstanceEntry, } from "../providerInstances"; +import { useUiStateStore } from "../uiStateStore"; + +export function handleMarkThreadUnreadShortcut( + event: Pick, + threadRef: ScopedThreadRef | null, + completedAt: string | null | undefined, +) { + event.preventDefault(); + event.stopPropagation(); + if (!threadRef) return; + useUiStateStore.getState().markThreadUnread(scopedThreadKey(threadRef), completedAt); +} export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project"; export const MAX_HIDDEN_MOUNTED_TERMINAL_THREADS = 10; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 39c091e7bebf..ae5b9ed04020 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -402,6 +402,7 @@ import { import { MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, agentControlledBrowserCloseConfirmation, + handleMarkThreadUnreadShortcut, branchMismatchKey, buildExpiredTerminalContextToastCopy, buildLocalDraftThread, @@ -1562,7 +1563,6 @@ 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, @@ -6658,10 +6658,11 @@ export default function ChatView(props: ChatViewProps) { } if (command === "thread.markUnread") { - event.preventDefault(); - event.stopPropagation(); - if (!isServerThread || !activeThreadKey) return; - markThreadUnread(activeThreadKey, activeLatestTurn?.completedAt); + handleMarkThreadUnreadShortcut( + event, + isServerThread ? activeThreadRef : null, + activeLatestTurn?.completedAt, + ); return; } @@ -6830,7 +6831,6 @@ export default function ChatView(props: ChatViewProps) { activeThreadPinned, activeThreadSettled, activeLatestTurn?.completedAt, - markThreadUnread, canInterruptRunningThread, activeThreadKey, terminalUiState.terminalOpen, From 4aaed4a162708ea44173047ec0a212e133cf8b9f Mon Sep 17 00:00:00 2001 From: id0sch Date: Thu, 17 Sep 2026 06:23:04 +0000 Subject: [PATCH 4/4] Revert "test(web): cover mark unread shortcut handler" This reverts commit c1b8fae69875740619f81dc21d14be6111e2cdfe. --- .../web/src/components/ChatView.logic.test.ts | 44 +------------------ apps/web/src/components/ChatView.logic.ts | 14 +----- apps/web/src/components/ChatView.tsx | 12 ++--- 3 files changed, 8 insertions(+), 62 deletions(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index f4bfe86f7ea1..c226a3a65c78 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -16,11 +16,10 @@ import { Atom, AsyncResult } from "effect/unstable/reactivity"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentThreadDetails } from "../state/threads"; -import { useUiStateStore } from "../uiStateStore"; import type { Thread, ThreadShell, TurnDiffSummary } from "../types"; import { deriveProviderInstanceEntries, NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates"; -import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { type RightPanelSurface, pullRequestSurface, @@ -35,7 +34,6 @@ import { MAX_HIDDEN_MOUNTED_PREVIEW_THREADS, MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, agentControlledBrowserCloseConfirmation, - handleMarkThreadUnreadShortcut, branchMismatchKey, buildExpiredTerminalContextToastCopy, buildLoadingThreadFromShell, @@ -2621,43 +2619,3 @@ describe("worktree setup visibility", () => { ).toEqual(base); }); }); - -describe("mark unread shortcut", () => { - const threadRef = scopeThreadRef(EnvironmentId.make("env-1"), ThreadId.make("thread-1")); - const threadKey = scopedThreadKey(threadRef); - const otherKey = scopedThreadKey({ ...threadRef, environmentId: EnvironmentId.make("env-2") }); - const completedAt = "2026-02-25T12:30:00.000Z"; - - beforeEach(() => { - useUiStateStore.setState({ - threadLastVisitedAtById: { [threadKey]: completedAt, [otherKey]: completedAt }, - }); - }); - afterEach(() => useUiStateStore.setState({ threadLastVisitedAtById: {} })); - - it("marks only the scoped active thread unread and consumes the key event", () => { - const event = new Event("keydown", { cancelable: true, bubbles: true }); - handleMarkThreadUnreadShortcut(event, threadRef, completedAt); - - expect(event.defaultPrevented).toBe(true); - expect(event.cancelBubble).toBe(true); - expect(useUiStateStore.getState().threadLastVisitedAtById).toEqual({ - [threadKey]: "2026-02-25T12:29:59.999Z", - [otherKey]: completedAt, - }); - - const unreadState = useUiStateStore.getState(); - handleMarkThreadUnreadShortcut(event, threadRef, completedAt); - expect(useUiStateStore.getState()).toBe(unreadState); - }); - - it.each([ - { ref: null, completion: completedAt }, - { ref: threadRef, completion: undefined }, - { ref: threadRef, completion: null }, - ])("leaves visit state unchanged for $ref / $completion", ({ ref, completion }) => { - const state = useUiStateStore.getState(); - handleMarkThreadUnreadShortcut(new Event("keydown"), ref, completion); - expect(useUiStateStore.getState()).toBe(state); - }); -}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 91753f6c0670..1dbe11f6c4c2 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -21,7 +21,7 @@ import { WORKTREE_SETUP_ACTIVITY_KIND, WorktreeSetupSnapshot, } from "@t3tools/contracts"; -import { parseScopedThreadKey, scopedThreadKey } from "@t3tools/client-runtime/environment"; +import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; import { resolveAssetUrl } from "@t3tools/client-runtime/state/assets"; import { squashAtomCommandFailure, @@ -60,18 +60,6 @@ import { resolveSelectableProviderInstanceEntry, type ProviderInstanceEntry, } from "../providerInstances"; -import { useUiStateStore } from "../uiStateStore"; - -export function handleMarkThreadUnreadShortcut( - event: Pick, - threadRef: ScopedThreadRef | null, - completedAt: string | null | undefined, -) { - event.preventDefault(); - event.stopPropagation(); - if (!threadRef) return; - useUiStateStore.getState().markThreadUnread(scopedThreadKey(threadRef), completedAt); -} export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project"; export const MAX_HIDDEN_MOUNTED_TERMINAL_THREADS = 10; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ae5b9ed04020..39c091e7bebf 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -402,7 +402,6 @@ import { import { MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, agentControlledBrowserCloseConfirmation, - handleMarkThreadUnreadShortcut, branchMismatchKey, buildExpiredTerminalContextToastCopy, buildLocalDraftThread, @@ -1563,6 +1562,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, @@ -6658,11 +6658,10 @@ export default function ChatView(props: ChatViewProps) { } if (command === "thread.markUnread") { - handleMarkThreadUnreadShortcut( - event, - isServerThread ? activeThreadRef : null, - activeLatestTurn?.completedAt, - ); + event.preventDefault(); + event.stopPropagation(); + if (!isServerThread || !activeThreadKey) return; + markThreadUnread(activeThreadKey, activeLatestTurn?.completedAt); return; } @@ -6831,6 +6830,7 @@ export default function ChatView(props: ChatViewProps) { activeThreadPinned, activeThreadSettled, activeLatestTurn?.completedAt, + markThreadUnread, canInterruptRunningThread, activeThreadKey, terminalUiState.terminalOpen,