mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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("?");
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user