Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/bundled/agents/AgentDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
Agents,
AgentConversation,
RegisteredAgentType,
TranscriptRequest,
} from "../../features/agents/service";
import {
AgentArchiveRetry,
Expand Down Expand Up @@ -78,6 +79,8 @@ export function AgentDetails({
messageAction,
conversation,
initialActivity,
initialTranscript,
close,
communities,
}: {
agents: Agents;
Expand All @@ -90,6 +93,9 @@ export function AgentDetails({
messageAction: (variant: "glass" | "subtle") => ReactNode;
conversation?: AgentConversation | undefined;
initialActivity?: boolean;
initialTranscript?: TranscriptRequest | undefined;
/** Closes the panel showing the agent. */
close?: () => void;
communities?: CommunityReader | undefined;
}) {
const { pack, src: backdrop } = useAgentPortraitSource(agent, agent.relay);
Expand Down Expand Up @@ -375,6 +381,8 @@ export function AgentDetails({
channels={channels}
conversation={conversation}
initialActivity={initialActivity}
initialTranscript={initialTranscript}
close={close}
settingsHost={settings.settingsHost}
onDiscard={settings.discard}
memories={memories}
Expand Down
3 changes: 3 additions & 0 deletions src/bundled/agents/AgentTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
AgentSettingsHost,
Agents,
RegisteredAgentType,
TranscriptRequest,
} from "../../features/agents/service";
import type { AgentMemories } from "../../features/agents/MemoryPanel";
import type { CommunityReader } from "../../features/communities/service";
Expand All @@ -27,6 +28,8 @@ export function AgentTabs(props: {
settingsContent: ReactNode;
conversation?: AgentConversation | undefined;
initialActivity?: boolean | undefined;
initialTranscript?: TranscriptRequest | undefined;
close?: (() => void) | undefined;
onDiscard?: () => void;
}) {
const settingsContent = (
Expand Down
1 change: 1 addition & 0 deletions src/bundled/builderlab/adapter-type/Activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function ActivityFeed({
transcript={transcript}
agentName={agent.name}
state={(turnId) => state.get(turnId)}
follow={false}
before={
<>
<Note>
Expand Down
49 changes: 49 additions & 0 deletions src/bundled/claude-code/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,55 @@ it("lists saved conversations and shows one's transcript, or that it is gone", a
expect(save).not.toHaveBeenCalled();
});

it("opens the conversation's transcript when opened from its activity, once", async () => {
const root = "e".repeat(64);
localStorage.setItem(
"buzz.claude-code.sessions.v1",
JSON.stringify({
[agent().key]: { [`channel/${root}`]: { id: "kept", seen: 0, at: 2 } },
}),
);
const transcript = JSON.stringify({
type: "assistant",
uuid: "u1",
timestamp: "2026-10-09T10:00:05Z",
message: { content: [{ type: "text", text: "Two files." }] },
});
const Activity = tab(
installed({ files: { "./-Users-me--buzz/kept.jsonl": transcript } }).type,
1,
);
const conversation = { channelId: "channel", rootId: root };
const { unmount } = render(
<Activity
agent={agent()}
save={vi.fn()}
conversation={conversation}
initialTranscript={{ opening: 1, done: () => undefined }}
/>,
);
const user = userEvent.setup();
expect(await screen.findByText("Two files.")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Back to profile" }));
expect(
screen.getByRole("button", {
name: "View transcript: channel",
}),
).toBeVisible();
expect(screen.queryByText("Two files.")).toBeNull();
unmount();
// Opened from the profile itself, it lists conversations instead.
render(
<Activity agent={agent()} save={vi.fn()} conversation={conversation} />,
);
expect(
screen.getByRole("button", {
name: "View transcript: channel",
}),
).toBeVisible();
expect(screen.queryByText("Two files.")).toBeNull();
});

it("shows the transcript and that sessions may be missing when some history cannot be read", async () => {
localStorage.setItem(
"buzz.claude-code.sessions.v1",
Expand Down
12 changes: 11 additions & 1 deletion src/bundled/claude-code/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,13 @@ export function createTabs({
function ActivityTab({
agent,
conversation,
initialTranscript,
close,
}: {
agent: AgentViewProps<Config>["agent"];
conversation?: AgentViewProps<Config>["conversation"];
initialTranscript?: AgentViewProps<Config>["initialTranscript"];
close?: AgentViewProps<Config>["close"];
}) {
const sessions = useSyncExternalStore(
(listener) => runtime.subscribe(listener),
Expand All @@ -731,7 +735,12 @@ export function createTabs({
const running = busy.get(key);
return running === undefined ? undefined : running ? "Working" : "Idle";
};
const { open, show, back } = useOpenTranscript(conversation, label);
const { open, show, back } = useOpenTranscript(
conversation,
label,
initialTranscript && { initial: here, ...initialTranscript },
close,
);
return (
<div className={activityStyles.activity}>
{conversation && (
Expand Down Expand Up @@ -803,6 +812,7 @@ export function createTabs({
onBack={back}
>
<TranscriptView
key={open.key}
agent={agent}
conversation={open}
working={busy.get(open.key) ?? false}
Expand Down
104 changes: 103 additions & 1 deletion src/bundled/codex/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "@testing-library/react";
import { StrictMode } from "react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { pubkeyOf } from "../../features/agents/membership";
import { ownerReads } from "../../features/agents/reads";
import {
Expand Down Expand Up @@ -384,6 +384,108 @@ it("brings an open transcript's tab back, and closes it when the profile moves",
expect(tab(transcript)).toHaveAttribute("aria-selected", "true");
});

describe("opened from activity", () => {
const root = "e".repeat(64);
const transcript = "#general · thread eeeeeeee";
const opened = () => {
const f = fixture({
saved: {
[JSON.stringify(["channel", root])]: {
threadId: "kept",
workspace: "/w",
at: 2,
},
},
});
const close = vi.fn();
const done = vi.fn();
// Each call is a new opening, as each Working now click is.
let openings = 0;
const panel = (done?: () => void) => (
<PanelWorkspace
value="profile"
select={() => undefined}
tabMenu={{
key: "channel",
closeAll: () => false,
closeOthers: () => false,
}}
items={[
{
id: "profile",
label: "Sol",
close: () => undefined,
content: (
<f.tabs.ActivityTab
{...f.props()}
conversation={{ channelId: "channel", rootId: root }}
{...(done && {
initialTranscript: { opening: ++openings, done },
})}
close={close}
/>
),
},
]}
/>
);
return { close, done, panel };
};

it("shows the transcript, and closing it closes the panel", async () => {
const { close, done, panel } = opened();
render(panel(done));
expect(
await screen.findByRole("tab", { name: transcript }),
).toHaveAttribute("aria-selected", "true");
expect(done).toHaveBeenCalled();
await userEvent.click(
screen.getByRole("button", { name: `Close ${transcript} tab` }),
);
expect(close).toHaveBeenCalledOnce();
});

it("keeps the panel when the profile's other tabs are closed", async () => {
const user = userEvent.setup();
const { close, done, panel } = opened();
render(panel(done));
await screen.findByRole("tab", { name: transcript });
await user.pointer({
target: screen.getByRole("tab", { name: "Sol" }),
keys: "[MouseRight]",
});
await user.click(
await screen.findByRole("menuitem", { name: "Close other tabs" }),
);
expect(screen.queryByRole("tab", { name: transcript })).toBeNull();
expect(close).not.toHaveBeenCalled();
});

it("shows the transcript again when the kept profile is opened again", async () => {
const user = userEvent.setup();
const { done, panel } = opened();
const view = render(panel(done));
await screen.findByRole("tab", { name: transcript });
await user.pointer({
target: screen.getByRole("tab", { name: "Sol" }),
keys: "[MouseRight]",
});
await user.click(
await screen.findByRole("menuitem", { name: "Close other tabs" }),
);
// The panel offered its request, so it no longer asks.
view.rerender(panel());
expect(screen.queryByRole("tab", { name: transcript })).toBeNull();
// Working now opens the same profile again: a new request.
const again = vi.fn();
view.rerender(panel(again));
expect(
await screen.findByRole("tab", { name: transcript }),
).toHaveAttribute("aria-selected", "true");
expect(again).toHaveBeenCalledOnce();
});
});

it("finds the thread's Codex session another build started", async () => {
const root = "f".repeat(64);
const tag = sessionTag("a".repeat(64), { channelId: "channel", root });
Expand Down
13 changes: 11 additions & 2 deletions src/bundled/codex/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -814,9 +814,13 @@ export function createTabs(
function ActivityTab({
agent,
conversation,
initialTranscript,
close,
}: {
agent: AgentViewProps<Config>["agent"];
conversation?: AgentViewProps<Config>["conversation"];
initialTranscript?: AgentViewProps<Config>["initialTranscript"];
close?: AgentViewProps<Config>["close"];
}) {
const sessions = useSyncExternalStore(runtime.subscribe, () =>
runtime.sessions(agent.key),
Expand All @@ -832,7 +836,12 @@ export function createTabs(
const recent = conversations
.filter((item) => item.key !== here?.key)
.slice(0, CONVERSATION_LIMIT);
const { open, show, back } = useOpenTranscript(conversation, label);
const { open, show, back } = useOpenTranscript(
conversation,
label,
initialTranscript && { initial: here, ...initialTranscript },
close,
);
const saved = new Set(conversations.map((row) => row.key));
const views = new Map(sessions.map((view) => [view.key, view]));
const unbound = sessions.filter((view) => !saved.has(view.key));
Expand Down Expand Up @@ -935,7 +944,7 @@ export function createTabs(
backLabel="Back to profile"
onBack={back}
>
<TranscriptView agent={agent} conversation={open} />
<TranscriptView key={open.key} agent={agent} conversation={open} />
</PanelSubview>
)}
</div>
Expand Down
12 changes: 11 additions & 1 deletion src/bundled/goose/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,13 @@ export function createTabs({
function ActivityTab({
agent,
conversation,
initialTranscript,
close,
}: {
agent: AgentViewProps<Config>["agent"];
conversation?: AgentViewProps<Config>["conversation"];
initialTranscript?: AgentViewProps<Config>["initialTranscript"];
close?: AgentViewProps<Config>["close"];
}) {
const workspace = readConfig(agent.config).workspace;
const live = useSyncExternalStore(
Expand Down Expand Up @@ -862,7 +866,12 @@ export function createTabs({
row.title
? titled(agent.name, row.title)
: `${row.channelId.slice(0, 8)} · ${row.root ? `thread ${row.root.slice(0, 8)}` : "channel"}`;
const { open, show, back } = useOpenTranscript(conversation, label);
const { open, show, back } = useOpenTranscript(
conversation,
label,
initialTranscript && { initial: here, ...initialTranscript },
close,
);
const rows = (recent.value ?? [])
.filter((row) => row.sessionId !== here?.sessionId)
.slice(0, RECENT_LIMIT)
Expand Down Expand Up @@ -955,6 +964,7 @@ export function createTabs({
onBack={back}
>
<TranscriptView
key={open.key}
agent={agent}
row={open}
working={busy(open.sessionId) ? open.sessionId : undefined}
Expand Down
20 changes: 20 additions & 0 deletions src/bundled/pi/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,26 @@ it("saves a key on Enter without saving the agent's settings", async () => {
expect(save).not.toHaveBeenCalled();
});

it("settles a transcript request without opening it once Pi's late listing arrives", async () => {
const runtime = fakeRuntime();
const { ActivityTab } = createTabs({ setup: new PiSetup(fakePi()), runtime });
const done = vi.fn();
render(
<ActivityTab
agent={AGENT}
conversation={{ channelId: "chan", rootId: "root" }}
initialTranscript={{ opening: 1, done }}
/>,
);
expect(done).toHaveBeenCalled();
const here = await screen.findByRole("region", { name: "This thread" });
expect(
within(here).getByRole("button", { name: /^View transcript/ }),
).toBeVisible();
expect(runtime.transcript).not.toHaveBeenCalled();
expect(screen.queryByText("Hello from chan/root")).toBeNull();
});

it("lists Pi's conversations in the Activity tab, and shows a transcript", async () => {
const runtime = fakeRuntime();
const { ActivityTab } = createTabs({ setup: new PiSetup(fakePi()), runtime });
Expand Down
Loading
Loading