diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx
index afe8b883bf..bcf0a6a511 100644
--- a/desktop/src/app/AppShell.tsx
+++ b/desktop/src/app/AppShell.tsx
@@ -684,10 +684,9 @@ export function AppShell() {
open: isCompanionWindow ? undefined : settingsOpen,
});
useMarkAsReadShortcuts({
- activeChannelId: ownedEffects.markAsReadShortcuts
- ? (activeChannel?.id ?? null)
- : null,
+ activeChannelId: activeChannel?.id ?? null,
activeChannelLastMessageAt: activeChannel?.lastMessageAt,
+ enabled: ownedEffects.markAsReadShortcuts,
markAllChannelsRead,
markChannelRead,
selectedView,
diff --git a/desktop/src/app/useMarkAsReadShortcuts.test.mjs b/desktop/src/app/useMarkAsReadShortcuts.test.mjs
new file mode 100644
index 0000000000..75f3032ae5
--- /dev/null
+++ b/desktop/src/app/useMarkAsReadShortcuts.test.mjs
@@ -0,0 +1,73 @@
+import assert from "node:assert/strict";
+import { after, afterEach, before, test } from "node:test";
+
+import { JSDOM } from "jsdom";
+
+const dom = new JSDOM("
", {
+ url: "http://localhost",
+});
+
+before(() => {
+ Object.assign(globalThis, {
+ document: dom.window.document,
+ HTMLElement: dom.window.HTMLElement,
+ IS_REACT_ACT_ENVIRONMENT: true,
+ KeyboardEvent: dom.window.KeyboardEvent,
+ window: dom.window,
+ });
+});
+
+afterEach(async () => {
+ const { cleanup } = await import("@testing-library/react");
+ cleanup();
+});
+
+after(() => dom.window.close());
+
+async function renderShortcuts(enabled) {
+ const { renderHook } = await import("@testing-library/react");
+ const { useMarkAsReadShortcuts } = await import(
+ "./useMarkAsReadShortcuts.ts"
+ );
+ const calls = [];
+ const view = renderHook(() =>
+ useMarkAsReadShortcuts({
+ activeChannelId: "general",
+ activeChannelLastMessageAt: "2026-08-24T00:00:00.000Z",
+ enabled,
+ markAllChannelsRead: () => calls.push("all"),
+ markChannelRead: () => calls.push("channel"),
+ selectedView: "channel",
+ }),
+ );
+ return { calls, view };
+}
+
+function pressEscape({ shiftKey = false } = {}) {
+ window.dispatchEvent(
+ new KeyboardEvent("keydown", {
+ bubbles: true,
+ cancelable: true,
+ key: "Escape",
+ shiftKey,
+ }),
+ );
+}
+
+test("disabled shortcuts ignore Escape and Shift+Escape", async () => {
+ const { calls } = await renderShortcuts(false);
+
+ pressEscape();
+ pressEscape({ shiftKey: true });
+
+ assert.deepEqual(calls, []);
+});
+
+test("enabled shortcuts preserve channel and all-channel actions", async () => {
+ const { calls } = await renderShortcuts(true);
+
+ pressEscape();
+ pressEscape({ shiftKey: true });
+
+ assert.deepEqual(calls, ["channel", "all"]);
+});
diff --git a/desktop/src/app/useMarkAsReadShortcuts.ts b/desktop/src/app/useMarkAsReadShortcuts.ts
index d2499d80bb..2daa079707 100644
--- a/desktop/src/app/useMarkAsReadShortcuts.ts
+++ b/desktop/src/app/useMarkAsReadShortcuts.ts
@@ -9,6 +9,7 @@ export function useMarkAsReadShortcuts({
markAllChannelsRead,
markChannelRead,
selectedView,
+ enabled = true,
}: {
activeChannelId: string | null;
activeChannelLastMessageAt: string | null | undefined;
@@ -18,8 +19,11 @@ export function useMarkAsReadShortcuts({
lastMessageAt: string | null | undefined,
) => void;
selectedView: string;
+ enabled?: boolean;
}) {
React.useEffect(() => {
+ if (!enabled) return;
+
function handleKeyDown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
if (event.defaultPrevented) return;
@@ -51,6 +55,7 @@ export function useMarkAsReadShortcuts({
}, [
activeChannelId,
activeChannelLastMessageAt,
+ enabled,
markAllChannelsRead,
markChannelRead,
selectedView,
diff --git a/desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs b/desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs
new file mode 100644
index 0000000000..620bec568f
--- /dev/null
+++ b/desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs
@@ -0,0 +1,26 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { shouldShowAgentSessionUnavailable } from "./ChannelPane.helpers.ts";
+
+test("unavailable activity state is exclusive to dedicated windows", () => {
+ const unresolvedSession = {
+ openAgentSessionPubkey: "agent-pubkey",
+ selectedAgent: null,
+ };
+
+ assert.equal(
+ shouldShowAgentSessionUnavailable({
+ ...unresolvedSession,
+ isDedicatedActivityWindow: true,
+ }),
+ true,
+ );
+ assert.equal(
+ shouldShowAgentSessionUnavailable({
+ ...unresolvedSession,
+ isDedicatedActivityWindow: false,
+ }),
+ false,
+ );
+});
diff --git a/desktop/src/features/channels/ui/ChannelPane.helpers.ts b/desktop/src/features/channels/ui/ChannelPane.helpers.ts
index cb0600a28a..c46369dbb3 100644
--- a/desktop/src/features/channels/ui/ChannelPane.helpers.ts
+++ b/desktop/src/features/channels/ui/ChannelPane.helpers.ts
@@ -57,6 +57,22 @@ export function isChannelCreatedSystemMessage(message: TimelineMessage) {
}
}
+export function shouldShowAgentSessionUnavailable({
+ isDedicatedActivityWindow,
+ openAgentSessionPubkey,
+ selectedAgent,
+}: {
+ isDedicatedActivityWindow: boolean;
+ openAgentSessionPubkey: string | null;
+ selectedAgent: unknown;
+}): boolean {
+ return (
+ isDedicatedActivityWindow &&
+ openAgentSessionPubkey !== null &&
+ selectedAgent == null
+ );
+}
+
export function mentionsKnownAgent(
mentionPubkeys: string[],
knownAgentPubkeys: ReadonlySet,
diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx
index 131c9df0e1..74ac1b6463 100644
--- a/desktop/src/features/channels/ui/ChannelPane.tsx
+++ b/desktop/src/features/channels/ui/ChannelPane.tsx
@@ -44,7 +44,10 @@ import {
WelcomeComposerGuidanceLayer,
} from "@/features/channels/ui/WelcomeComposerBanner";
import { useWelcomeComposerBanner } from "@/features/channels/ui/useWelcomeComposerBanner";
-import { mentionsKnownAgent } from "@/features/channels/ui/ChannelPane.helpers";
+import {
+ mentionsKnownAgent,
+ shouldShowAgentSessionUnavailable,
+} from "@/features/channels/ui/ChannelPane.helpers";
import { HuddleStartingView, HuddleTranscriptIntro } from "@/features/huddle";
import { useChannelIntro } from "@/features/channels/ui/useChannelIntro";
import type { ChannelPaneProps } from "@/features/channels/ui/ChannelPane.types";
@@ -81,6 +84,7 @@ export const ChannelPane = React.memo(function ChannelPane({
historyExhausted,
isFetchingOlder,
isHuddleTranscript = false,
+ isDedicatedActivityWindow = false,
followThreadById,
isFollowingThread,
isFollowingThreadById,
@@ -915,7 +919,11 @@ export const ChannelPane = React.memo(function ChannelPane({
);
return wrapAux(panel, "agent-session-thread-panel");
})()
- ) : isSinglePanelView && openAgentSessionPubkey ? (
+ ) : shouldShowAgentSessionUnavailable({
+ isDedicatedActivityWindow,
+ openAgentSessionPubkey,
+ selectedAgent,
+ }) ? (
0 &&
channelContentWidthPx < AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX;
- // A dedicated activity window is the panel itself, not a responsive channel
- // view. Keep the channel timeline out of that window at every width.
const isSinglePanelView =
isDedicatedActivityWindow ||
(isNarrowPanelViewport &&
@@ -871,6 +870,7 @@ export function ChannelScreen({
onOpenMembers={handleOpenMembersSidebar}
isFetchingOlder={isFetchingOlder}
isHuddleTranscript={isHuddleTranscript}
+ isDedicatedActivityWindow={isDedicatedActivityWindow}
entranceMessageId={welcomeEntranceMessageId}
onEntranceMessageComplete={handleWelcomeEntranceComplete}
welcomeKickoffStage={welcomeKickoffStage}
diff --git a/desktop/src/features/channels/ui/useChannelOpenReadState.ts b/desktop/src/features/channels/ui/useChannelOpenReadState.ts
index 85a18b1e14..a8336cd584 100644
--- a/desktop/src/features/channels/ui/useChannelOpenReadState.ts
+++ b/desktop/src/features/channels/ui/useChannelOpenReadState.ts
@@ -39,7 +39,16 @@ export function useChannelOpenReadState(
useAppShell();
React.useEffect(() => {
- if (!activeChannelId || !enabled || isChannelMember === false) return;
+ if (
+ !shouldAdvanceChannelReadState({
+ activeChannelId,
+ enabled,
+ isChannelMember,
+ }) ||
+ !activeChannelId
+ ) {
+ return;
+ }
for (const itemId of getTopLevelInboxUnreadOverrideIds(
locallyUnreadFeedItems,
activeChannelId,
diff --git a/desktop/src/features/channels/ui/useHuddleReadMarker.test.mjs b/desktop/src/features/channels/ui/useHuddleReadMarker.test.mjs
new file mode 100644
index 0000000000..90d4512e42
--- /dev/null
+++ b/desktop/src/features/channels/ui/useHuddleReadMarker.test.mjs
@@ -0,0 +1,64 @@
+import assert from "node:assert/strict";
+import { after, afterEach, before, test } from "node:test";
+
+import { JSDOM } from "jsdom";
+
+const dom = new JSDOM("", {
+ url: "http://localhost",
+});
+
+before(() => {
+ Object.assign(globalThis, {
+ document: dom.window.document,
+ HTMLElement: dom.window.HTMLElement,
+ IS_REACT_ACT_ENVIRONMENT: true,
+ window: dom.window,
+ });
+});
+
+afterEach(async () => {
+ const { cleanup } = await import("@testing-library/react");
+ cleanup();
+});
+
+after(() => dom.window.close());
+
+const topLevelMessage = {
+ content: "message",
+ created_at: 1_724_457_600,
+ id: "message-id",
+ kind: 9,
+ pubkey: "author",
+ sig: "signature",
+ tags: [["h", "general"]],
+};
+
+async function mountReadMarker(enabled) {
+ const { renderHook } = await import("@testing-library/react");
+ const { useHuddleReadMarker } = await import("./useHuddleReadMarker.ts");
+ const calls = [];
+ const view = renderHook(() =>
+ useHuddleReadMarker({
+ activeChannelId: "general",
+ activeChannelIsMember: true,
+ enabled,
+ isHuddleTranscript: false,
+ markChannelRead: (...args) => calls.push(args),
+ messages: [topLevelMessage],
+ resolvedMessages: [topLevelMessage],
+ }),
+ );
+ return { calls, view };
+}
+
+test("disabled activity companions do not advance the huddle read marker", async () => {
+ const { calls } = await mountReadMarker(false);
+ assert.deepEqual(calls, []);
+});
+
+test("enabled channel screens retain the top-level read marker", async () => {
+ const { calls } = await mountReadMarker(true);
+ assert.deepEqual(calls, [
+ ["general", "2024-08-24T00:00:00.000Z", { topLevelOnly: true }],
+ ]);
+});
diff --git a/desktop/src/features/channels/ui/useHuddleReadMarker.ts b/desktop/src/features/channels/ui/useHuddleReadMarker.ts
index a9188fa27f..4c17cd0d5a 100644
--- a/desktop/src/features/channels/ui/useHuddleReadMarker.ts
+++ b/desktop/src/features/channels/ui/useHuddleReadMarker.ts
@@ -13,6 +13,7 @@ type HuddleReadMarkerOptions = {
activeChannelId: string | null;
activeChannelIsMember: boolean | undefined;
isHuddleTranscript: boolean;
+ enabled?: boolean;
markChannelRead: MarkChannelRead;
messages: RelayEvent[] | undefined;
resolvedMessages: RelayEvent[];
@@ -22,6 +23,7 @@ export function useHuddleReadMarker({
activeChannelId,
activeChannelIsMember,
isHuddleTranscript,
+ enabled = true,
markChannelRead,
messages,
resolvedMessages,
@@ -65,7 +67,7 @@ export function useHuddleReadMarker({
const lastHuddleReadKeyRef = React.useRef(null);
React.useEffect(() => {
- if (!activeChannelId || activeChannelIsMember === false) return;
+ if (!enabled || !activeChannelId || activeChannelIsMember === false) return;
const huddleReadKey = hasFlattenedHuddleReplies
? `${activeChannelId}:${huddleReadAt}`
: null;
@@ -82,6 +84,7 @@ export function useHuddleReadMarker({
}, [
activeChannelId,
activeChannelIsMember,
+ enabled,
hasFlattenedHuddleReplies,
huddleReadAt,
markChannelRead,