Preserve transcript tool-run view choices

Co-authored-by: ss-quality-00 <75789fffd704a5265cd65462cfc263f44561872fbebcea3a131a2a5f511465d2@buzz.block.builderlab.xyz>
Signed-off-by: ss-quality-00 <75789fffd704a5265cd65462cfc263f44561872fbebcea3a131a2a5f511465d2@buzz.block.builderlab.xyz>
This commit is contained in:
ss-quality-00
2026-08-21 18:09:48 -07:00
committed by ss-dev-01
parent 52123ee68f
commit c176335fef
7 changed files with 208 additions and 6 deletions

View File

@@ -49,6 +49,7 @@ export default defineConfig({
"**/profile-active-turn.spec.ts",
"**/config-bridge-screenshots.spec.ts",
"**/observer-feed-screenshots.spec.ts",
"**/transcript-tool-run-quality.smoke.spec.ts",
"**/core-memory-screenshots.spec.ts",
"**/activity-scope-label-screenshots.spec.ts",
"**/welcome-agent-modal-screenshots.spec.ts",

View File

@@ -29,11 +29,16 @@ export function ToolItem({
agentPubkey,
item,
profiles,
expanded,
onExpansionChange,
}: AgentTranscriptIdentityProps & {
item: Extract<TranscriptItem, { type: "tool" }>;
profiles?: UserProfileLookup;
expanded?: boolean;
onExpansionChange?: (expanded: boolean) => void;
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const [localExpanded, setLocalExpanded] = React.useState(false);
const isExpanded = expanded ?? localExpanded;
const hasArgs = Object.keys(item.args).length > 0;
const hasResult = item.result.trim().length > 0;
const canonicalToolName = item.buzzToolName ?? item.toolName;
@@ -52,9 +57,14 @@ export function ToolItem({
const agentResolvedAvatarUrl = agentProfile?.avatarUrl ?? agentAvatarUrl;
const handleToggle = React.useCallback(
(event: React.SyntheticEvent<HTMLDetailsElement>) => {
setIsExpanded(event.currentTarget.open);
const nextExpanded = event.currentTarget.open;
if (onExpansionChange) {
onExpansionChange(nextExpanded);
} else {
setLocalExpanded(nextExpanded);
}
},
[],
[onExpansionChange],
);
if (compactSummary.presentation === "message") {

View File

@@ -533,7 +533,13 @@ function SameKindSummaryItem({
// the group card never has to import this module back. Not memoized: the
// group card calls it during render and never uses its identity as a
// dependency, and memoizing would need this component as its own dep.
const renderChild = (child: TranscriptToolRunChildSegment) =>
const renderChild = (
child: TranscriptToolRunChildSegment,
expansion?: {
expanded: boolean;
onExpansionChange: (expanded: boolean) => void;
},
) =>
child.kind === "summary" ? (
<SameKindSummaryItem
agentAvatarUrl={agentAvatarUrl}
@@ -550,7 +556,9 @@ function SameKindSummaryItem({
agentPubkey={agentPubkey}
item={child.item}
key={child.item.id}
onExpansionChange={expansion?.onExpansionChange}
profiles={profiles}
expanded={expansion?.expanded}
/>
);
@@ -948,17 +956,23 @@ const TranscriptItemView = React.memo(function TranscriptItemView({
agentName,
agentPubkey,
item,
onExpansionChange,
profiles,
expanded,
}: AgentTranscriptIdentityProps & {
item: TranscriptItem;
profiles?: UserProfileLookup;
expanded?: boolean;
onExpansionChange?: (expanded: boolean) => void;
}) {
return (
<TranscriptActivityItem
agentAvatarUrl={agentAvatarUrl}
agentName={agentName}
agentPubkey={agentPubkey}
expanded={expanded}
item={item}
onExpansionChange={onExpansionChange}
profiles={profiles}
/>
);

View File

@@ -23,6 +23,7 @@ import {
import {
setToolRunGroupExpanded,
setToolRunGroupInternalStepsShown,
setToolRunGroupItemExpanded,
} from "./agentSessionToolRunViewState";
import { useToolRunGroupViewState } from "./useToolRunGroupViewState";
import { formatTranscriptTimestampTitle } from "./agentSessionUtils";
@@ -121,7 +122,13 @@ export type TranscriptToolRunGroupProps = {
label: React.ReactNode;
showTimestamp: boolean;
/** Renders one child segment. Passed in to avoid a list↔group import cycle. */
renderChild: (child: TranscriptToolRunChildSegment) => React.ReactNode;
renderChild: (
child: TranscriptToolRunChildSegment,
expansion?: {
expanded: boolean;
onExpansionChange: (expanded: boolean) => void;
},
) => React.ReactNode;
};
/**
@@ -200,7 +207,24 @@ export function TranscriptToolRunGroup({
{label}
<ToolRunGroupStatusBadge failureCount={failureCount} status={status} />
<ActivityRowContent className="flex flex-col gap-0.5">
{visibleChildren.map((child) => renderChild(child))}
{visibleChildren.map((child) =>
renderChild(
child,
child.kind === "item" && child.item.type === "tool"
? {
expanded: state.expandedItemIds.includes(child.item.id),
onExpansionChange: (expanded) =>
update((previous) =>
setToolRunGroupItemExpanded(
previous,
child.item.id,
expanded,
),
),
}
: undefined,
),
)}
{hiddenItems.length > 0 ? (
<button
className="mt-0.5 self-start text-xs text-muted-foreground underline decoration-dotted hover:text-foreground"

View File

@@ -12,6 +12,9 @@ export type AgentTranscriptIdentityProps = {
export type ActivityRenderClassItemProps = AgentTranscriptIdentityProps & {
item: TranscriptItem;
profiles?: UserProfileLookup;
/** Controlled detail expansion for rows whose state must survive regrouping. */
expanded?: boolean;
onExpansionChange?: (expanded: boolean) => void;
};
export type ActivityRenderClassPresenter =

View File

@@ -29,6 +29,7 @@ import {
restoreActiveAgentTurnsForCommunity,
} from "@/features/agents/activeAgentTurnsStore";
import { resetAgentWorkingSignal } from "@/features/agents/agentWorkingSignal";
import { resetAgentSessionToolRunViewState } from "@/features/agents/ui/agentSessionToolRunViewStore";
import { resetAgentObserverStore } from "@/features/agents/observerRelayStore";
import { resetAvatarPresentations } from "@/features/profile/avatarPresentationStore";
import { resetAvatarProfileSync } from "@/features/profile/avatarProfileSync";
@@ -60,6 +61,7 @@ async function resetCommunityState({
resetRateLimitGate();
clearAllDrafts();
resetAgentObserverStore();
resetAgentSessionToolRunViewState();
resetActiveAgentTurnsStore();
resetAgentWorkingSignal();
if (isTauri() && isMacPlatform()) {

View File

@@ -0,0 +1,148 @@
import { expect, test } from "@playwright/test";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
const AGENT_PUBKEY = TEST_IDENTITIES.tyler.pubkey;
const CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301";
const NOW = new Date("2025-06-15T12:00:00Z").toISOString();
const MANAGED_AGENTS = [
{
pubkey: AGENT_PUBKEY,
name: "Observer Agent",
status: "running" as const,
channelNames: ["agents"],
},
];
async function openFeed(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.waitForFunction(
() => typeof window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ === "function",
);
await page.getByTestId("channel-agents").click();
const row = page
.getByTestId("message-row")
.filter({ has: page.getByText("Observer Agent", { exact: false }) })
.first();
await row.getByRole("button").first().click();
await page.getByTestId(`user-profile-view-activity-${AGENT_PUBKEY}`).click();
return page.getByTestId("agent-session-thread-panel");
}
function event(
seq: number,
toolCallId: string,
update: Record<string, unknown>,
) {
return {
seq,
timestamp: NOW,
kind: "acp_read",
agentIndex: 0,
channelId: CHANNEL_ID,
sessionId: "session-quality",
turnId: "turn-quality",
payload: {
method: "session/update",
params: {
sessionId: "session-quality",
update: { toolCallId, ...update },
},
},
};
}
async function seed(
page: import("@playwright/test").Page,
events: ReturnType<typeof event>[],
) {
await page.evaluate(
({ pubkey, events }) =>
window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__?.({
agentPubkey: pubkey,
events,
}),
{ pubkey: AGENT_PUBKEY, events },
);
}
test("tool-run group stays readable through live completion and user expansion", async ({
page,
}) => {
await installMockBridge(page, { managedAgents: MANAGED_AGENTS });
const panel = await openFeed(page);
await seed(page, [
event(1, "read-a", {
sessionUpdate: "tool_call",
status: "executing",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/a.ts" },
}),
event(2, "read-b", {
sessionUpdate: "tool_call",
status: "executing",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/b.ts" },
}),
event(3, "read-c", {
sessionUpdate: "tool_call",
status: "executing",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/c.ts" },
}),
]);
const group = panel.getByTestId("transcript-same-kind-summary");
await expect(group).toHaveAttribute("open", "");
await expect(
group.getByTestId("tool-run-group-status-running"),
).toBeVisible();
await group.locator("summary").first().click();
await expect(group).not.toHaveAttribute("open", "");
await group.locator("summary").first().click();
await expect(group).toHaveAttribute("open", "");
await seed(page, [
event(4, "read-a", {
sessionUpdate: "tool_call_update",
status: "completed",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/a.ts" },
rawOutput: "a",
}),
event(5, "read-b", {
sessionUpdate: "tool_call_update",
status: "completed",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/b.ts" },
rawOutput: "b",
}),
event(6, "read-c", {
sessionUpdate: "tool_call_update",
status: "completed",
title: "read_file",
kind: "read_file",
rawInput: { path: "src/c.ts" },
rawOutput: "c",
}),
]);
await expect(group).toHaveAttribute("open", "");
const child = group
.getByTestId("transcript-tool-item")
.first()
.locator("details");
await child.locator("summary").click();
await expect(child).toHaveAttribute("open", "");
await group.locator("summary").first().click();
await group.locator("summary").first().click();
await expect(child).toHaveAttribute("open", "");
});