fix(desktop): keep activity companions pinned

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-24 02:20:47 -07:00
parent d9392d2573
commit 1502d6215e
10 changed files with 132 additions and 33 deletions

View File

@@ -3,6 +3,7 @@ import { describe, it } from "node:test";
import {
acceptsNativeDeepLinks,
agentActivityCompanionCoordinates,
companionCommunityBootstrap,
companionCommunityIdForHash,
companionWindowKindForLabel,
@@ -23,6 +24,35 @@ describe("companionWindowKindForLabel", () => {
});
});
describe("agentActivityCompanionCoordinates", () => {
const coordinates = {
community: "community-one",
agentSession: "agent-one",
agentSessionChannel: "channel-one",
};
it("returns every immutable activity companion coordinate", () => {
assert.deepEqual(
agentActivityCompanionCoordinates("agent-activity", coordinates),
coordinates,
);
});
it("rejects non-activity windows and incomplete coordinates", () => {
assert.equal(
agentActivityCompanionCoordinates("huddle", coordinates),
undefined,
);
assert.equal(
agentActivityCompanionCoordinates("agent-activity", {
community: coordinates.community,
agentSession: coordinates.agentSession,
}),
undefined,
);
});
});
describe("acceptsNativeDeepLinks", () => {
it("reserves the pending-link queue for the main realm", () => {
assert.equal(acceptsNativeDeepLinks(null), true);

View File

@@ -24,6 +24,42 @@ export function currentCompanionWindowKind(): CompanionWindowKind | null {
}
}
export type AgentActivityCompanionCoordinates = {
community: string;
agentSession: string;
agentSessionChannel: string;
};
export function agentActivityCompanionCoordinates(
companionKind: CompanionWindowKind | null,
search: Record<string, unknown>,
): AgentActivityCompanionCoordinates | undefined {
if (
companionKind !== "agent-activity" ||
typeof search.community !== "string" ||
typeof search.agentSession !== "string" ||
typeof search.agentSessionChannel !== "string"
) {
return undefined;
}
return {
community: search.community,
agentSession: search.agentSession,
agentSessionChannel: search.agentSessionChannel,
};
}
/** Return the immutable route coordinates owned by this activity companion. */
export function currentAgentActivityCompanionCoordinates(
search: Record<string, unknown>,
): AgentActivityCompanionCoordinates | undefined {
return agentActivityCompanionCoordinates(
currentCompanionWindowKind(),
search,
);
}
/** Community encoded into a companion bootstrap hash. */
export function companionCommunityIdForHash(hash: string): string | null {
const query = hash.indexOf("?");

View File

@@ -7,7 +7,7 @@ import {
} from "@tanstack/react-router";
import { openSearchHitWithNavigation } from "@/app/navigation/searchHitNavigation";
import { currentCompanionWindowKind } from "@/app/companionWindow";
import { currentAgentActivityCompanionCoordinates } from "@/app/companionWindow";
import type { SearchHit } from "@/shared/api/types";
type NavigationBehavior = {
@@ -21,18 +21,9 @@ export function useAppNavigation() {
const navigate = useNavigate();
const location = useLocation();
const canGoBack = useCanGoBack();
const companionSearch = location.search as Record<string, unknown>;
const companionCoordinates =
currentCompanionWindowKind() === "agent-activity" &&
typeof companionSearch.community === "string" &&
typeof companionSearch.agentSession === "string" &&
typeof companionSearch.agentSessionChannel === "string"
? {
community: companionSearch.community,
agentSession: companionSearch.agentSession,
agentSessionChannel: companionSearch.agentSessionChannel,
}
: undefined;
const companionCoordinates = currentAgentActivityCompanionCoordinates(
location.search as Record<string, unknown>,
);
const commitNavigation = React.useCallback(
async (

View File

@@ -1,5 +1,6 @@
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { Markdown } from "@/shared/ui/markdown";
import { activityWindowMarkdownInteractive } from "./activityWindowPresentation";
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
import { formatTranscriptTimestampTitle } from "../agentSessionUtils";
import type { TranscriptItem } from "../agentSessionTypes";
@@ -65,6 +66,7 @@ function MessageItem({
<Markdown
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
content={text || " "}
interactive={activityWindowMarkdownInteractive()}
/>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { Markdown } from "@/shared/ui/markdown";
import { activityWindowMarkdownInteractive } from "./activityWindowPresentation";
import {
ActivityRow,
ActivityRowContent,
@@ -41,6 +42,7 @@ export function PlanActivity(props: ActivityRenderClassItemProps) {
<Markdown
className="leading-5"
content={props.item.text.trim() || "No plan details."}
interactive={activityWindowMarkdownInteractive()}
/>
</ActivityRowContent>
</ActivityRow>

View File

@@ -1,4 +1,5 @@
import { Markdown } from "@/shared/ui/markdown";
import { activityWindowMarkdownInteractive } from "./activityWindowPresentation";
import {
ActivityRow,
ActivityRowContent,
@@ -26,6 +27,7 @@ export function ThoughtActivity(props: ActivityRenderClassItemProps) {
<Markdown
className="leading-5"
content={props.item.text.trim() || " "}
interactive={activityWindowMarkdownInteractive()}
/>
</ActivityRowContent>
</ActivityRow>

View File

@@ -8,9 +8,11 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { cn } from "@/shared/lib/cn";
import { useProfilePanel } from "@/shared/context/ProfilePanelContext";
import { Markdown } from "@/shared/ui/markdown";
import { activityWindowMarkdownInteractive } from "./activityWindowPresentation";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
import type { TranscriptItem } from "../agentSessionTypes";
import { isAgentActivityWindow } from "../../lib/agentActivityWindow";
import { MessageLinkHoverCue } from "./MessageLinkHoverCue";
import { useTranscriptBubbleOverflow } from "./useTranscriptBubbleOverflow";
@@ -33,12 +35,16 @@ export function UserMessageBubble({
const { goChannel } = useAppNavigation();
const { openProfilePanel } = useProfilePanel();
const isCompactPreview = variant === "compactPreview";
const isDedicatedActivityWindow = isAgentActivityWindow();
const shouldClampBubble = !isCompactPreview;
const [bubbleRef, hasBubbleOverflow] =
useTranscriptBubbleOverflow(shouldClampBubble);
const text = item.text.trim();
const messageLink =
shouldClampBubble && item.channelId && item.messageId
shouldClampBubble &&
!isDedicatedActivityWindow &&
item.channelId &&
item.messageId
? { channelId: item.channelId, messageId: item.messageId }
: null;
const authorProfile = item.authorPubkey
@@ -98,7 +104,9 @@ export function UserMessageBubble({
data-role="user-message"
data-testid="transcript-user-message"
>
{isCompactPreview ? null : item.authorPubkey && openProfilePanel ? (
{isCompactPreview ? null : item.authorPubkey &&
openProfilePanel &&
!isDedicatedActivityWindow ? (
<button
aria-label={`Open ${authorLabel} profile`}
className="pointer-events-auto order-last ml-2 mt-1 size-7 shrink-0 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
@@ -148,6 +156,7 @@ export function UserMessageBubble({
<Markdown
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
content={text || " "}
interactive={activityWindowMarkdownInteractive()}
mediaInset
/>
{children}

View File

@@ -0,0 +1,6 @@
import { currentCompanionWindowKind } from "@/app/companionWindow";
/** Dedicated feeds render navigation targets as readable, non-clickable content. */
export function activityWindowMarkdownInteractive(): boolean {
return currentCompanionWindowKind() !== "agent-activity";
}

View File

@@ -1,3 +1,4 @@
import { currentAgentActivityCompanionCoordinates } from "@/app/companionWindow";
import * as React from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
@@ -85,7 +86,10 @@ export function useHistorySearchState<K extends string>(keys: readonly K[]) {
nextSearch[key] = value;
}
}
return nextSearch;
return {
...nextSearch,
...currentAgentActivityCompanionCoordinates(previousSearch),
};
},
replace: flush.replace,
resetScroll: false,

View File

@@ -60,6 +60,25 @@ test("keeps dedicated activity windows pinned to their original channel", async
{
seq: 2,
timestamp: new Date().toISOString(),
kind: "acp_read",
agentIndex: 0,
channelId,
sessionId: "session-user-message",
turnId: "turn-user-message",
payload: {
method: "session/update",
params: {
update: {
sessionUpdate: "user_message_chunk",
authorPubkey: repositoryOwnerPubkey,
content: { type: "text", text: "Please inspect the feed" },
},
},
},
},
{
seq: 3,
timestamp: new Date().toISOString(),
kind: "turn_started",
agentIndex: 0,
channelId,
@@ -68,7 +87,7 @@ test("keeps dedicated activity windows pinned to their original channel", async
payload: { source: "channel", triggeringEventIds: [] },
},
{
seq: 3,
seq: 4,
timestamp: new Date().toISOString(),
kind: "acp_read",
agentIndex: 0,
@@ -104,27 +123,25 @@ test("keeps dedicated activity windows pinned to their original channel", async
"Other channel activity must stay hidden",
);
const channelReference = panel.getByRole("button", {
name: "Open channel general",
});
await expect(channelReference).toBeVisible();
const channelReference = panel.locator("[data-channel-link]");
await expect(channelReference).toContainText("general");
await expect(
panel.getByRole("button", { name: "Open channel general" }),
).toHaveCount(0);
const activityWindowUrl = page.url();
await channelReference.click();
await expect(page).toHaveURL(activityWindowUrl);
await expect(
panel.getByRole("button", { name: "Open alice profile" }),
).toHaveCount(0);
await expect(panel).toBeVisible();
await expect(panel).toContainText("Activity · #agents");
await expect(panel).not.toContainText(
"Other channel activity must stay hidden",
);
await expect(page).toHaveURL(/agentSession=/);
await expect(page).toHaveURL(/agentSessionChannel=/);
const repositoryChip = panel.getByRole("button", {
name: "Open repository relay-tools",
});
await expect(repositoryChip).toBeVisible();
await repositoryChip.click();
await expect(panel).toContainText("relay-tools");
await expect(
panel.getByRole("button", { name: "Open repository relay-tools" }),
).toHaveCount(0);
await expect(page).toHaveURL(activityWindowUrl);
await expect(panel).toBeVisible();
await page.getByTestId("agent-session-settings-menu-trigger").click();
const rawFeedToggle = page.getByTestId("agent-session-toggle-raw-feed");