From dd9996d0aa5c3a988513cee07c5deed06fcbcd4e Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:45:53 +0800 Subject: [PATCH] MUL-4014: persist transcript filters and expansion (#4884) * feat(transcript): persist log view preferences Co-authored-by: multica-agent * fix(transcript): wrap modal header controls Co-authored-by: multica-agent --------- Co-authored-by: multica-agent --- packages/core/agents/stores/index.ts | 1 + .../stores/transcript-view-store.test.ts | 47 ++- .../agents/stores/transcript-view-store.ts | 49 +++- .../agent-transcript-dialog.test.tsx | 240 ++++++++++++++++ .../agent-transcript-dialog.tsx | 267 ++++++++++++++---- packages/views/locales/en/agents.json | 6 +- packages/views/locales/ja/agents.json | 6 +- packages/views/locales/ko/agents.json | 6 +- packages/views/locales/zh-Hans/agents.json | 6 +- 9 files changed, 569 insertions(+), 59 deletions(-) create mode 100644 packages/views/common/task-transcript/agent-transcript-dialog.test.tsx diff --git a/packages/core/agents/stores/index.ts b/packages/core/agents/stores/index.ts index 49cc10093..4d3eeb9d8 100644 --- a/packages/core/agents/stores/index.ts +++ b/packages/core/agents/stores/index.ts @@ -13,5 +13,6 @@ export { } from "./view-store"; export { useTranscriptViewStore, + type TranscriptFilterKey, type TranscriptSortDirection, } from "./transcript-view-store"; diff --git a/packages/core/agents/stores/transcript-view-store.test.ts b/packages/core/agents/stores/transcript-view-store.test.ts index 4c89e3848..6bf21f35d 100644 --- a/packages/core/agents/stores/transcript-view-store.test.ts +++ b/packages/core/agents/stores/transcript-view-store.test.ts @@ -2,12 +2,20 @@ import { beforeEach, describe, expect, it } from "vitest"; import { useTranscriptViewStore } from "./transcript-view-store"; beforeEach(() => { - useTranscriptViewStore.setState({ sortDirection: "chronological" }); + useTranscriptViewStore.setState({ + sortDirection: "chronological", + preserveFilters: false, + selectedFilterKeys: [], + defaultExpanded: false, + }); }); describe("useTranscriptViewStore", () => { - it("defaults to chronological so existing readers see no behavior change", () => { + it("defaults to chronological, unfiltered, and collapsed", () => { expect(useTranscriptViewStore.getState().sortDirection).toBe("chronological"); + expect(useTranscriptViewStore.getState().preserveFilters).toBe(false); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual([]); + expect(useTranscriptViewStore.getState().defaultExpanded).toBe(false); }); it("setSortDirection switches between the two known directions", () => { @@ -19,4 +27,39 @@ describe("useTranscriptViewStore", () => { setSortDirection("chronological"); expect(useTranscriptViewStore.getState().sortDirection).toBe("chronological"); }); + + it("stores filter preferences as unique serializable keys", () => { + const { setPreserveFilters, setSelectedFilterKeys, toggleFilterKey, clearFilterKeys } = + useTranscriptViewStore.getState(); + + setPreserveFilters(true); + setSelectedFilterKeys(["thinking", "tool:terminal", "thinking", ""]); + expect(useTranscriptViewStore.getState().preserveFilters).toBe(true); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual([ + "thinking", + "tool:terminal", + ]); + + toggleFilterKey("thinking"); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual(["tool:terminal"]); + + toggleFilterKey("text"); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual([ + "tool:terminal", + "text", + ]); + + clearFilterKeys(); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual([]); + }); + + it("stores the default-expanded preference", () => { + const { setDefaultExpanded } = useTranscriptViewStore.getState(); + + setDefaultExpanded(true); + expect(useTranscriptViewStore.getState().defaultExpanded).toBe(true); + + setDefaultExpanded(false); + expect(useTranscriptViewStore.getState().defaultExpanded).toBe(false); + }); }); diff --git a/packages/core/agents/stores/transcript-view-store.ts b/packages/core/agents/stores/transcript-view-store.ts index 97baba148..cbab15153 100644 --- a/packages/core/agents/stores/transcript-view-store.ts +++ b/packages/core/agents/stores/transcript-view-store.ts @@ -5,22 +5,67 @@ import { createJSONStorage, persist } from "zustand/middleware"; import { defaultStorage } from "../../platform/storage"; export type TranscriptSortDirection = "chronological" | "newest_first"; +export type TranscriptFilterKey = string; interface TranscriptViewState { sortDirection: TranscriptSortDirection; + preserveFilters: boolean; + selectedFilterKeys: TranscriptFilterKey[]; + defaultExpanded: boolean; setSortDirection: (dir: TranscriptSortDirection) => void; + setPreserveFilters: (preserve: boolean) => void; + setSelectedFilterKeys: (keys: TranscriptFilterKey[]) => void; + toggleFilterKey: (key: TranscriptFilterKey) => void; + clearFilterKeys: () => void; + setDefaultExpanded: (expanded: boolean) => void; +} + +const DEFAULTS = { + sortDirection: "chronological" as TranscriptSortDirection, + preserveFilters: false, + selectedFilterKeys: [] as TranscriptFilterKey[], + defaultExpanded: false, +}; + +function uniqueFilterKeys(keys: TranscriptFilterKey[]): TranscriptFilterKey[] { + return Array.from(new Set(keys.filter((key) => key.length > 0))); } export const useTranscriptViewStore = create()( persist( (set) => ({ - sortDirection: "chronological", + ...DEFAULTS, setSortDirection: (sortDirection) => set({ sortDirection }), + setPreserveFilters: (preserveFilters) => set({ preserveFilters }), + setSelectedFilterKeys: (selectedFilterKeys) => + set({ selectedFilterKeys: uniqueFilterKeys(selectedFilterKeys) }), + toggleFilterKey: (key) => + set((state) => ({ + selectedFilterKeys: state.selectedFilterKeys.includes(key) + ? state.selectedFilterKeys.filter((candidate) => candidate !== key) + : [...state.selectedFilterKeys, key], + })), + clearFilterKeys: () => set({ selectedFilterKeys: [] }), + setDefaultExpanded: (defaultExpanded) => set({ defaultExpanded }), }), { name: "multica_transcript_view", storage: createJSONStorage(() => defaultStorage), - partialize: (state) => ({ sortDirection: state.sortDirection }), + partialize: (state) => ({ + sortDirection: state.sortDirection, + preserveFilters: state.preserveFilters, + selectedFilterKeys: state.selectedFilterKeys, + defaultExpanded: state.defaultExpanded, + }), + merge: (persisted, current) => { + if (!persisted) return { ...current, ...DEFAULTS }; + const p = persisted as Partial; + return { + ...current, + ...p, + selectedFilterKeys: uniqueFilterKeys(p.selectedFilterKeys ?? []), + }; + }, }, ), ); diff --git a/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx b/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx new file mode 100644 index 000000000..f1a730bfc --- /dev/null +++ b/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx @@ -0,0 +1,240 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ButtonHTMLAttributes, ReactNode } from "react"; +import type { AgentTask } from "@multica/core/types/agent"; +import { useTranscriptViewStore } from "@multica/core/agents/stores"; +import { renderWithI18n } from "../../test/i18n"; +import { AgentTranscriptDialog } from "./agent-transcript-dialog"; +import type { TimelineItem } from "./build-timeline"; + +vi.mock("@multica/core/api", () => ({ + api: { + getAgent: vi.fn().mockResolvedValue(null), + listRuntimes: vi.fn().mockResolvedValue([]), + }, +})); + +vi.mock("../actor-avatar", () => ({ + ActorAvatar: () => , +})); + +vi.mock("@multica/ui/components/ui/dialog", () => ({ + Dialog: ({ open, children }: { open: boolean; children: ReactNode }) => + open ? <>{children} : null, + DialogContent: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, +})); + +vi.mock("@multica/ui/components/ui/dropdown-menu", () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuTrigger: ({ + children, + ...props + }: ButtonHTMLAttributes) => ( + + ), + DropdownMenuContent: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + DropdownMenuSeparator: () =>
, + DropdownMenuCheckboxItem: ({ + checked, + onCheckedChange, + children, + }: { + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + children: ReactNode; + }) => ( + + ), + DropdownMenuItem: ({ + children, + onClick, + className: _className, + }: ButtonHTMLAttributes) => ( + + ), +})); + +vi.mock("@multica/ui/components/ui/collapsible", async () => { + const React = await import("react"); + const Context = React.createContext<{ + open: boolean; + onOpenChange?: (open: boolean) => void; + }>({ open: false }); + + return { + Collapsible: ({ + open, + onOpenChange, + children, + }: { + open: boolean; + onOpenChange?: (open: boolean) => void; + children: ReactNode; + }) => ( + {children} + ), + CollapsibleTrigger: ({ + disabled, + children, + className: _className, + }: ButtonHTMLAttributes) => { + const ctx = React.useContext(Context); + return ( + + ); + }, + CollapsibleContent: ({ children }: { children: ReactNode }) => { + const ctx = React.useContext(Context); + return ctx.open ?
{children}
: null; + }, + }; +}); + +const baseTask: AgentTask = { + id: "task-1", + agent_id: "", + runtime_id: "", + issue_id: "issue-1", + status: "completed", + priority: 0, + dispatched_at: null, + started_at: "2026-06-08T08:00:00Z", + completed_at: "2026-06-08T08:01:00Z", + result: null, + error: null, + created_at: "2026-06-08T08:00:00Z", +}; + +const items: TimelineItem[] = [ + { + seq: 1, + type: "text", + content: "Agent summary\nAgent hidden detail", + }, + { + seq: 2, + type: "thinking", + content: "Thinking summary\nThinking hidden detail", + }, + { + seq: 3, + type: "tool_use", + tool: "terminal", + input: { command: "pnpm test" }, + }, +]; + +function renderDialog(dialogItems: TimelineItem[] = items) { + return renderWithI18n( + , + ); +} + +beforeEach(() => { + cleanup(); + useTranscriptViewStore.setState({ + sortDirection: "chronological", + preserveFilters: false, + selectedFilterKeys: [], + defaultExpanded: false, + }); +}); + +afterEach(() => { + cleanup(); +}); + +describe("AgentTranscriptDialog", () => { + it("preserves selected filters across dialog remounts when enabled", () => { + const first = renderDialog(); + + fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "Thinking" })); + fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "Preserve filters" })); + + expect(screen.queryByText("Agent summary")).not.toBeInTheDocument(); + expect(screen.getByText(/Thinking summary/)).toBeInTheDocument(); + expect(useTranscriptViewStore.getState().selectedFilterKeys).toEqual(["thinking"]); + + first.unmount(); + renderDialog(); + + expect(screen.queryByText("Agent summary")).not.toBeInTheDocument(); + expect(screen.getByText(/Thinking summary/)).toBeInTheDocument(); + }); + + it("ignores stale persisted filter keys that are not available in the current transcript", () => { + useTranscriptViewStore.setState({ + preserveFilters: true, + selectedFilterKeys: ["thinking"], + }); + + renderDialog([ + { + seq: 1, + type: "text", + content: "Only agent summary\nOnly agent hidden detail", + }, + ]); + + expect(screen.getByText("Only agent summary")).toBeInTheDocument(); + expect(screen.queryByText("No execution data recorded.")).not.toBeInTheDocument(); + }); + + it("expands and collapses every currently visible detailed row", () => { + renderDialog(); + + expect(screen.queryByText(/Agent hidden detail/)).not.toBeInTheDocument(); + expect(screen.queryByText(/"command": "pnpm test"/)).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Expand visible" })); + + expect(screen.getByText(/Agent hidden detail/)).toBeInTheDocument(); + expect(screen.getByText(/"command": "pnpm test"/)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Collapse visible" })); + + expect(screen.queryByText(/Agent hidden detail/)).not.toBeInTheDocument(); + expect(screen.queryByText(/"command": "pnpm test"/)).not.toBeInTheDocument(); + }); + + it("uses the default-expanded preference for newly opened transcripts", () => { + useTranscriptViewStore.setState({ defaultExpanded: true }); + + renderDialog(); + + expect(screen.getByText(/Agent hidden detail/)).toBeInTheDocument(); + expect(screen.getByText(/"command": "pnpm test"/)).toBeInTheDocument(); + }); +}); diff --git a/packages/views/common/task-transcript/agent-transcript-dialog.tsx b/packages/views/common/task-transcript/agent-transcript-dialog.tsx index f608259fa..fe545874d 100644 --- a/packages/views/common/task-transcript/agent-transcript-dialog.tsx +++ b/packages/views/common/task-transcript/agent-transcript-dialog.tsx @@ -35,7 +35,11 @@ import { } from "@multica/ui/components/ui/dropdown-menu"; import { ActorAvatar } from "../actor-avatar"; import { api } from "@multica/core/api"; -import { useTranscriptViewStore, type TranscriptSortDirection } from "@multica/core/agents/stores"; +import { + useTranscriptViewStore, + type TranscriptFilterKey, + type TranscriptSortDirection, +} from "@multica/core/agents/stores"; import type { AgentTask, Agent, AgentRuntime } from "@multica/core/types/agent"; import { redactSecrets } from "./redact"; import type { TimelineItem } from "./build-timeline"; @@ -105,6 +109,12 @@ function getEventLabel(item: TimelineItem): string { } } +function getItemFilterKey(item: TimelineItem): TranscriptFilterKey { + return item.tool && (item.type === "tool_use" || item.type === "tool_result") + ? `tool:${item.tool}` + : item.type; +} + function getEventSummary(item: TimelineItem): string { switch (item.type) { case "text": @@ -142,6 +152,16 @@ function getEventSummary(item: TimelineItem): string { } } +function hasEventDetail(item: TimelineItem): boolean { + return ( + (item.type === "tool_use" && !!item.input && Object.keys(item.input).length > 0) || + (item.type === "tool_result" && !!item.output && item.output.length > 0) || + (item.type === "thinking" && !!item.content && item.content.length > 0) || + (item.type === "text" && !!item.content && item.content.length > 0) || + (item.type === "error" && !!item.content && item.content.length > 0) + ); +} + function shortenPath(p: string): string { const parts = p.split("/"); if (parts.length <= 3) return p; @@ -183,11 +203,24 @@ export function AgentTranscriptDialog({ const [copiedWorkdir, setCopiedWorkdir] = useState(false); const [agentInfo, setAgentInfo] = useState(null); const [runtimeInfo, setRuntimeInfo] = useState(null); - const [selectedTools, setSelectedTools] = useState>(new Set()); + const [sessionFilterKeys, setSessionFilterKeys] = useState([]); + const [expandedSeqs, setExpandedSeqs] = useState>(() => new Set()); const sortDirection = useTranscriptViewStore((s) => s.sortDirection); const setSortDirection = useTranscriptViewStore((s) => s.setSortDirection); + const preserveFilters = useTranscriptViewStore((s) => s.preserveFilters); + const setPreserveFilters = useTranscriptViewStore((s) => s.setPreserveFilters); + const persistedFilterKeys = useTranscriptViewStore((s) => s.selectedFilterKeys); + const setPersistedFilterKeys = useTranscriptViewStore((s) => s.setSelectedFilterKeys); + const togglePersistedFilterKey = useTranscriptViewStore((s) => s.toggleFilterKey); + const clearPersistedFilterKeys = useTranscriptViewStore((s) => s.clearFilterKeys); + const defaultExpanded = useTranscriptViewStore((s) => s.defaultExpanded); + const setDefaultExpanded = useTranscriptViewStore((s) => s.setDefaultExpanded); const eventRefs = useRef>(new Map()); const scrollContainerRef = useRef(null); + const autoExpandedSeqsRef = useRef>(new Set()); + const initializedTaskRef = useRef(null); + const previousDefaultExpandedRef = useRef(defaultExpanded); + const selectedFilterKeys = preserveFilters ? persistedFilterKeys : sessionFilterKeys; // Derive filter options from each item: // tool_use / tool_result → filter value = tool, display = "tool:Bash" @@ -195,30 +228,35 @@ export function AgentTranscriptDialog({ const filterOptions = useMemo(() => { const options = new Map(); for (const item of items) { + const key = getItemFilterKey(item); if (item.tool && (item.type === "tool_use" || item.type === "tool_result")) { - const key = `tool:${item.tool}`; if (!options.has(key)) options.set(key, key); } else { - const value = item.type; - if (!options.has(value)) { - options.set(value, getEventLabel(item)); + if (!options.has(key)) { + options.set(key, getEventLabel(item)); } } } return Array.from(options.entries()).sort((a, b) => a[1].localeCompare(b[1])); }, [items]); - // Resolve filter key for each item — mirrors filterOptions derivation exactly - const itemFilterKey = (item: TimelineItem) => - item.tool && (item.type === "tool_use" || item.type === "tool_result") - ? `tool:${item.tool}` - : item.type; + const filterOptionKeys = useMemo( + () => new Set(filterOptions.map(([value]) => value)), + [filterOptions], + ); + + const activeFilterKeys = useMemo( + () => selectedFilterKeys.filter((key) => filterOptionKeys.has(key)), + [selectedFilterKeys, filterOptionKeys], + ); + + const activeFilterSet = useMemo(() => new Set(activeFilterKeys), [activeFilterKeys]); // Strict filter const filteredItems = useMemo(() => { - if (selectedTools.size === 0) return items; - return items.filter((item) => selectedTools.has(itemFilterKey(item))); - }, [items, selectedTools]); + if (activeFilterSet.size === 0) return items; + return items.filter((item) => activeFilterSet.has(getItemFilterKey(item))); + }, [items, activeFilterSet]); // Apply user-chosen sort direction. Reverse is a pure presentation concern — // the underlying timeline (and its seq numbers) is untouched, so copy/filter @@ -228,6 +266,37 @@ export function AgentTranscriptDialog({ [filteredItems, sortDirection], ); + const detailSeqs = useMemo( + () => displayItems.filter(hasEventDetail).map((item) => item.seq), + [displayItems], + ); + + const allVisibleDetailsExpanded = + detailSeqs.length > 0 && detailSeqs.every((seq) => expandedSeqs.has(seq)); + + useEffect(() => { + const switchedDefaultOn = + defaultExpanded && previousDefaultExpandedRef.current !== defaultExpanded; + previousDefaultExpandedRef.current = defaultExpanded; + + if (initializedTaskRef.current !== task.id || switchedDefaultOn) { + initializedTaskRef.current = task.id; + autoExpandedSeqsRef.current = new Set(defaultExpanded ? detailSeqs : []); + setExpandedSeqs(defaultExpanded ? new Set(detailSeqs) : new Set()); + return; + } + + if (!defaultExpanded) return; + + const unseen = detailSeqs.filter((seq) => !autoExpandedSeqsRef.current.has(seq)); + if (unseen.length === 0) return; + + for (const seq of unseen) { + autoExpandedSeqsRef.current.add(seq); + } + setExpandedSeqs((prev) => new Set([...prev, ...unseen])); + }, [task.id, defaultExpanded, detailSeqs]); + // Toggling direction is a manual user action; jump the scroll container back // to the top so the newest end of the timeline (per the chosen direction) is // immediately visible. Avoids stranding the user mid-scroll on the wrong end. @@ -303,18 +372,70 @@ export function AgentTranscriptDialog({ }); }, [displayItems]); - // Toggle tool filter - const toggleTool = useCallback((tool: string) => { - setSelectedTools((prev) => { + const toggleSessionFilterKey = useCallback((key: TranscriptFilterKey) => { + setSessionFilterKeys((prev) => { const next = new Set(prev); - if (next.has(tool)) next.delete(tool); - else next.add(tool); - return next; + if (next.has(key)) next.delete(key); + else next.add(key); + return Array.from(next); }); }, []); const clearFilters = useCallback(() => { - setSelectedTools(new Set()); + if (preserveFilters) { + clearPersistedFilterKeys(); + return; + } + setSessionFilterKeys([]); + }, [clearPersistedFilterKeys, preserveFilters]); + + const toggleFilterKey = useCallback( + (key: TranscriptFilterKey) => { + if (preserveFilters) { + togglePersistedFilterKey(key); + return; + } + toggleSessionFilterKey(key); + }, + [preserveFilters, togglePersistedFilterKey, toggleSessionFilterKey], + ); + + const handlePreserveFiltersChange = useCallback( + (next: boolean) => { + if (next) { + setPersistedFilterKeys(sessionFilterKeys); + } else { + setSessionFilterKeys(persistedFilterKeys); + } + setPreserveFilters(next); + }, + [persistedFilterKeys, sessionFilterKeys, setPersistedFilterKeys, setPreserveFilters], + ); + + const handleToggleVisibleExpanded = useCallback(() => { + for (const seq of detailSeqs) { + autoExpandedSeqsRef.current.add(seq); + } + setExpandedSeqs((prev) => { + if (allVisibleDetailsExpanded) { + const next = new Set(prev); + for (const seq of detailSeqs) { + next.delete(seq); + } + return next; + } + return new Set([...prev, ...detailSeqs]); + }); + }, [allVisibleDetailsExpanded, detailSeqs]); + + const handleRowExpandedChange = useCallback((seq: number, expanded: boolean) => { + autoExpandedSeqsRef.current.add(seq); + setExpandedSeqs((prev) => { + const next = new Set(prev); + if (expanded) next.add(seq); + else next.delete(seq); + return next; + }); }, []); // Duration @@ -326,25 +447,30 @@ export function AgentTranscriptDialog({ : null; const toolCount = items.filter((i) => i.type === "tool_use").length; + const copyTranscriptLabel = copied + ? t(($) => $.transcript.copied) + : activeFilterKeys.length > 0 + ? t(($) => $.transcript.copy_filtered) + : t(($) => $.transcript.copy_all); // Status display const statusBadge = isLive ? ( - + {t(($) => $.transcript.status_running)} ) : task.status === "completed" ? ( - + {t(($) => $.transcript.status_completed)} ) : task.status === "failed" ? ( - + {t(($) => $.transcript.status_failed)} ) : ( - + {task.status} ); @@ -360,21 +486,45 @@ export function AgentTranscriptDialog({ {/* ── Header ─────────────────────────────────────────────── */}
{/* Top row: agent name, status, actions */} -
-
+
+
{task.agent_id ? ( ) : ( -
+
)} - {agentName} + {agentName}
{statusBadge} -
+
+ {detailSeqs.length > 0 && ( + + )} {items.length > 1 && ( 0 && ( $.transcript.filter)} className={cn( - "flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors", - selectedTools.size > 0 + "flex shrink-0 items-center gap-1 rounded px-2 py-1 text-xs transition-colors", + activeFilterKeys.length > 0 ? "text-blue-600 dark:text-blue-400 bg-blue-500/10 hover:bg-blue-500/20" : "text-muted-foreground hover:text-foreground hover:bg-accent", )} > - {t(($) => $.transcript.filter)} - {selectedTools.size > 0 && ( + {t(($) => $.transcript.filter)} + {activeFilterKeys.length > 0 && ( - {selectedTools.size} + {activeFilterKeys.length} )} @@ -408,13 +559,26 @@ export function AgentTranscriptDialog({ {filterOptions.map(([value, label]) => ( toggleTool(value)} + checked={selectedFilterKeys.includes(value)} + onCheckedChange={() => toggleFilterKey(value)} > {label} ))} - {selectedTools.size > 0 && ( + + handlePreserveFiltersChange(checked === true)} + > + {t(($) => $.transcript.preserve_filters)} + + setDefaultExpanded(checked === true)} + > + {t(($) => $.transcript.default_expanded)} + + {selectedFilterKeys.length > 0 && ( <> @@ -428,15 +592,16 @@ export function AgentTranscriptDialog({ @@ -481,7 +646,7 @@ export function AgentTranscriptDialog({ {t(($) => $.transcript.tool_calls, { count: toolCount })} )} - {selectedTools.size > 0 + {activeFilterKeys.length > 0 ? t(($) => $.transcript.events_filtered, { shown: filteredItems.length, total: items.length }) : t(($) => $.transcript.events, { count: items.length })} @@ -570,6 +735,8 @@ export function AgentTranscriptDialog({ }} item={item} isSelected={selectedSeq === item.seq} + expanded={expandedSeqs.has(item.seq)} + onExpandedChange={(expanded) => handleRowExpandedChange(item.seq, expanded)} /> ))}
@@ -593,7 +760,7 @@ function SortDirectionToggle({ value, onChange, labels }: SortDirectionTogglePro