fix(desktop): make activity windows passive

Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-23 21:26:39 -07:00
committed by Rizz
parent f156835e83
commit 5f7f5031d1
11 changed files with 213 additions and 9 deletions

View File

@@ -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,

View File

@@ -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("<!doctype html><html><body></body></html>", {
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"]);
});

View File

@@ -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,

View File

@@ -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,
);
});

View File

@@ -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<string>,

View File

@@ -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,
}) ? (
<div
className="flex min-h-0 flex-1 items-center justify-center p-8 text-center"
data-testid="agent-session-unavailable"

View File

@@ -52,6 +52,7 @@ export type ChannelPaneProps = {
isFetchingOlder?: boolean;
/** A companion huddle window presents the channel only as a transcript. */
isHuddleTranscript?: boolean;
isDedicatedActivityWindow?: boolean;
isJoining?: boolean;
isSinglePanelView?: boolean;
isAgentSessionLoading?: boolean;

View File

@@ -257,6 +257,7 @@ export function ChannelScreen({
activeChannelId,
activeChannelIsMember: activeChannel?.isMember,
isHuddleTranscript,
enabled: !isDedicatedActivityWindow,
markChannelRead,
messages: messagesQuery.data,
resolvedMessages,
@@ -701,8 +702,6 @@ export function ChannelScreen({
const isNarrowPanelViewport =
channelContentWidthPx > 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}

View File

@@ -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,

View File

@@ -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("<!doctype html><html><body></body></html>", {
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 }],
]);
});

View File

@@ -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<string | null>(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,