diff --git a/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx b/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx index 6c0cfdb27..3491ef410 100644 --- a/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx +++ b/packages/views/common/task-transcript/agent-transcript-dialog.test.tsx @@ -306,9 +306,9 @@ describe("AgentTranscriptDialog", () => { renderDialog([], { task: liveTask, isLive: true }); - // The runtime's display name lives in the facts line's hover title; the - // visible text is the runtime mode. - await screen.findByTitle("hermes runtime"); + // Runtime detail now lives in the ⓘ popover; its trigger appearing proves + // the runtime loaded. The non-antigravity live state still waits. + await screen.findByRole("button", { name: "Run details" }); expect(screen.getByText("Waiting for events...")).toBeInTheDocument(); }); diff --git a/packages/views/common/task-transcript/agent-transcript-dialog.tsx b/packages/views/common/task-transcript/agent-transcript-dialog.tsx index 1b1722d73..90116b73c 100644 --- a/packages/views/common/task-transcript/agent-transcript-dialog.tsx +++ b/packages/views/common/task-transcript/agent-transcript-dialog.tsx @@ -14,18 +14,16 @@ import { Clock, Copy, Check, - Monitor, - Cloud, - Cpu, Filter, - Folder, ArrowDownNarrowWide, ArrowUpNarrowWide, ListCollapse, + Info, } from "lucide-react"; import { cn } from "@multica/ui/lib/utils"; import { copyText } from "@multica/ui/lib/clipboard"; import { Dialog, DialogContent, DialogTitle } from "@multica/ui/components/ui/dialog"; +import { Popover, PopoverContent, PopoverTrigger } from "@multica/ui/components/ui/popover"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@multica/ui/components/ui/collapsible"; import { DropdownMenu, @@ -133,6 +131,63 @@ function formatElapsedMs(ms: number): string { return `${minutes}m ${secs}s`; } +function formatRunTime(iso: string): string { + return new Date(iso).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +// ─── Run detail row (ⓘ popover) ───────────────────────────────────────────── +// One labeled fact in the diagnostic popover. When `onCopy` is given the whole +// row is a copy button (used for the workdir path). +function RunDetailRow({ + label, + value, + mono, + onCopy, + copied, + copyTitle, +}: { + label: string; + value: string; + mono?: boolean; + onCopy?: () => void; + copied?: boolean; + copyTitle?: string; +}) { + const valueClass = cn("min-w-0 select-text break-all text-foreground/80", mono && "font-mono"); + if (onCopy) { + return ( + + ); + } + return ( +
+ {label} + {value} +
+ ); +} + // ─── Main dialog ──────────────────────────────────────────────────────────── // Virtuoso mounts rows as direct children of its List element; carry the @@ -388,27 +443,95 @@ export function AgentTranscriptDialog({ ? 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} - - ); + // Status badge — full state machine, so queued/dispatched/cancelled render as + // proper labels instead of raw enum text. + const effectiveStatus = isLive ? "running" : task.status; + const statusBadge = (() => { + const base = "inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"; + switch (effectiveStatus) { + case "running": + return ( + + + {t(($) => $.transcript.status_running)} + + ); + case "completed": + return ( + + + {t(($) => $.transcript.status_completed)} + + ); + case "failed": + return ( + + + {t(($) => $.transcript.status_failed)} + + ); + case "cancelled": + return ( + + + {t(($) => $.transcript.status_cancelled)} + + ); + case "queued": + return ( + + {t(($) => $.transcript.status_queued)} + + ); + case "dispatched": + return ( + + {t(($) => $.transcript.status_dispatched)} + + ); + case "waiting_local_directory": + return ( + + {t(($) => $.transcript.status_waiting)} + + ); + default: + return ( + + {task.status} + + ); + } + })(); + + // Trigger source: one word answering "why does this run exist" — more useful + // up front than the runtime/provider diagnostics, which move to the ⓘ popover. + const triggerLabel = task.parent_task_id + ? t(($) => $.transcript.trigger_retry) + : task.kind === "comment" || task.trigger_comment_id + ? t(($) => $.transcript.trigger_comment) + : task.kind === "autopilot" || task.autopilot_run_id + ? t(($) => $.transcript.trigger_autopilot) + : task.kind === "chat" || task.chat_session_id + ? t(($) => $.transcript.trigger_chat) + : task.kind === "quick_create" + ? t(($) => $.transcript.trigger_quick_create) + : task.kind === "direct" || task.handoff_note + ? t(($) => $.transcript.trigger_direct) + : t(($) => $.transcript.trigger_initial); + + // Diagnostic detail for the ⓘ popover: everything a reader needs only when + // debugging this specific run, kept off the always-visible surface. + const providerLabel = runtimeInfo?.provider ? formatProvider(runtimeInfo.provider) : null; + const createdLabel = task.created_at ? formatRunTime(task.created_at) : null; + const startedLabel = task.started_at ? formatRunTime(task.started_at) : null; + const completedLabel = task.completed_at ? formatRunTime(task.completed_at) : null; + const hasRunDetails = + !!runtimeInfo || + !!task.relative_work_dir || + !!createdLabel || + !!startedLabel || + !!completedLabel; return ( @@ -418,124 +541,118 @@ export function AgentTranscriptDialog({ > {t(($) => $.transcript.dialog_title)} - {/* ── Header ─────────────────────────────────────────────── */} -
- {/* Identity row: outcome first, then who ran and on whose behalf. - Status is the one fact every viewer opens this dialog for, so it - anchors the left edge. Entities render through their existing - identity components (avatar + hover card), never ad-hoc chips. */} + {/* ── Header: identity only ────────────────────────────────── + Tier 1 — everything a viewer needs BEFORE reading: outcome + (status anchors the left), who ran it, why it exists (trigger), + and who's accountable. All diagnostics move to the ⓘ popover. */} +
{statusBadge} -
- {task.agent_id ? ( - - ) : ( -
- -
- )} - - {agentName || agentInfo?.name || ""} - +
+
+ {task.agent_id ? ( + + ) : ( +
+ +
+ )} + + {agentName || agentInfo?.name || ""} + +
+ {triggerLabel} + {/* Accountable member (MUL-4302 §9): whose behalf this run is on. */} +
- {/* Accountable member (MUL-4302 §9): whose behalf this run is on. */} - - -
- {/* Facts line: quantitative run facts as plain dot-separated - typography — borders belong to controls, not to information. - Diagnostic details (hostname, workdir path) hide behind - hover/copy affordances instead of occupying the line. */} -
- {runtimeInfo?.provider && ( +
+ {hasRunDetails && ( + + $.transcript.run_info)} + title={t(($) => $.transcript.run_info)} + className="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" + > + + + +
+ {t(($) => $.transcript.run_info)} +
+
+ {runtimeInfo && ( + $.transcript.details_runtime)} value={runtimeInfo.name} /> + )} + {providerLabel && ( + $.transcript.details_provider)} value={providerLabel} /> + )} + {runtimeInfo && ( + $.transcript.details_mode)} value={runtimeInfo.runtime_mode} /> + )} + {task.relative_work_dir && ( + $.transcript.details_workdir)} + value={task.relative_work_dir} + mono + onCopy={handleCopyWorkdir} + copied={copiedWorkdir} + copyTitle={t(($) => $.transcript.copy_workdir)} + /> + )} + {createdLabel && ( + $.transcript.details_created)} value={createdLabel} /> + )} + {startedLabel && ( + $.transcript.details_started)} value={startedLabel} /> + )} + {completedLabel && ( + $.transcript.details_completed)} value={completedLabel} /> + )} +
+
+
+ )} + +
+
+
+ + {/* ── List toolbar: read-before-you-read summary (left) + controls + (right). Duration + event count fill the left, so the row balances + instead of leaving dead space. ── */} +
+
+ {duration && ( - - {formatProvider(runtimeInfo.provider)} + + {duration} )} - {runtimeInfo && ( - <> - - - {runtimeInfo.runtime_mode === "cloud" ? ( - - ) : ( - - )} - {runtimeInfo.runtime_mode} - - - )} - {duration && ( - <> - - - - {duration} - - - )} + {duration && } + + {activeFilterKeys.length > 0 + ? t(($) => $.transcript.events_filtered, { shown: filteredItems.length, total: items.length }) + : t(($) => $.transcript.events, { count: items.length })} + {toolCount > 0 && ( <> {t(($) => $.transcript.tool_calls, { count: toolCount })} )} - - - {activeFilterKeys.length > 0 - ? t(($) => $.transcript.events_filtered, { shown: filteredItems.length, total: items.length }) - : t(($) => $.transcript.events, { count: items.length })} - - {task.created_at && ( - <> - - - {new Date(task.created_at).toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - })} - - - )} - {/* Working directory — server-derived display path. Only - `relative_work_dir` is safe to render / put in title / copy: - the server has already stripped $HOME and the username out of - it, while the absolute `task.work_dir` never reaches the DOM. - The path itself stays out of the line (routinely long); the - icon copies it and the tooltip shows it. */} - {task.relative_work_dir && ( - - )}
-
- - {/* ── List toolbar: every control operating the list below ── */} -
+
{items.length > 0 && ( : } {copyTranscriptLabel} +
{/* ── Timeline progress bar ─────────────────────────────── */} diff --git a/packages/views/common/task-transcript/trace-event-presenter.ts b/packages/views/common/task-transcript/trace-event-presenter.ts index 01588b279..99aa9b487 100644 --- a/packages/views/common/task-transcript/trace-event-presenter.ts +++ b/packages/views/common/task-transcript/trace-event-presenter.ts @@ -88,7 +88,7 @@ const SHELL_WRAPPER_PATTERN = export function stripShellWrapper(command: string): string { const match = SHELL_WRAPPER_PATTERN.exec(command.trim()); - return match ? match[2] : command; + return match?.[2] ?? command; } function clip(value: string, max: number): string { diff --git a/packages/views/locales/en/agents.json b/packages/views/locales/en/agents.json index 769b37227..820ed4bfb 100644 --- a/packages/views/locales/en/agents.json +++ b/packages/views/locales/en/agents.json @@ -788,7 +788,27 @@ "density_collapsed": "Collapse all", "density_collapsed_desc": "One line per event — the most compact scan", "no_output": "No output", - "show_all": "Show all" + "show_all": "Show all", + "status_queued": "Queued", + "status_dispatched": "Dispatched", + "status_cancelled": "Cancelled", + "status_waiting": "Waiting", + "trigger_initial": "Initial run", + "trigger_comment": "From a comment", + "trigger_autopilot": "Autopilot", + "trigger_retry": "Retry", + "trigger_chat": "From chat", + "trigger_quick_create": "Quick create", + "trigger_direct": "Direct assignment", + "run_info": "Run details", + "details_runtime": "Runtime", + "details_provider": "Provider", + "details_mode": "Mode", + "details_workdir": "Workdir", + "details_created": "Created", + "details_started": "Started", + "details_completed": "Completed", + "copy_workdir": "Copy working directory" }, "task_failure": { "agent_error": "Agent execution error", diff --git a/packages/views/locales/ja/agents.json b/packages/views/locales/ja/agents.json index a262f5520..74219ce9a 100644 --- a/packages/views/locales/ja/agents.json +++ b/packages/views/locales/ja/agents.json @@ -668,7 +668,27 @@ "density_collapsed": "すべて折りたたむ", "density_collapsed_desc": "1 行 1 イベントの最もコンパクトな表示", "no_output": "出力なし", - "show_all": "すべて表示" + "show_all": "すべて表示", + "status_queued": "待機列", + "status_dispatched": "ディスパッチ済み", + "status_cancelled": "キャンセル", + "status_waiting": "待機中", + "trigger_initial": "初回実行", + "trigger_comment": "コメントから", + "trigger_autopilot": "Autopilot", + "trigger_retry": "再試行", + "trigger_chat": "チャットから", + "trigger_quick_create": "クイック作成", + "trigger_direct": "直接アサイン", + "run_info": "実行の詳細", + "details_runtime": "ランタイム", + "details_provider": "プロバイダー", + "details_mode": "モード", + "details_workdir": "作業ディレクトリ", + "details_created": "作成", + "details_started": "開始", + "details_completed": "完了", + "copy_workdir": "作業ディレクトリをコピー" }, "task_failure": { "agent_error": "エージェント実行エラー", diff --git a/packages/views/locales/ko/agents.json b/packages/views/locales/ko/agents.json index 5c07570e9..c1244527f 100644 --- a/packages/views/locales/ko/agents.json +++ b/packages/views/locales/ko/agents.json @@ -676,7 +676,27 @@ "density_collapsed": "모두 접기", "density_collapsed_desc": "이벤트당 한 줄, 가장 간결한 보기", "no_output": "출력 없음", - "show_all": "모두 보기" + "show_all": "모두 보기", + "status_queued": "대기열", + "status_dispatched": "디스패치됨", + "status_cancelled": "취소됨", + "status_waiting": "대기 중", + "trigger_initial": "최초 실행", + "trigger_comment": "댓글에서", + "trigger_autopilot": "Autopilot", + "trigger_retry": "재시도", + "trigger_chat": "채팅에서", + "trigger_quick_create": "빠른 생성", + "trigger_direct": "직접 지정", + "run_info": "실행 세부정보", + "details_runtime": "런타임", + "details_provider": "제공자", + "details_mode": "모드", + "details_workdir": "작업 디렉터리", + "details_created": "생성", + "details_started": "시작", + "details_completed": "완료", + "copy_workdir": "작업 디렉터리 복사" }, "task_failure": { "agent_error": "에이전트 실행 오류", diff --git a/packages/views/locales/zh-Hans/agents.json b/packages/views/locales/zh-Hans/agents.json index a33a55058..caaad72e1 100644 --- a/packages/views/locales/zh-Hans/agents.json +++ b/packages/views/locales/zh-Hans/agents.json @@ -772,7 +772,27 @@ "density_collapsed": "全部收起", "density_collapsed_desc": "一行一事件,最紧凑的扫读视图", "no_output": "无输出", - "show_all": "显示全部" + "show_all": "显示全部", + "status_queued": "排队中", + "status_dispatched": "已派发", + "status_cancelled": "已取消", + "status_waiting": "等待中", + "trigger_initial": "初次运行", + "trigger_comment": "评论触发", + "trigger_autopilot": "Autopilot", + "trigger_retry": "重试", + "trigger_chat": "来自对话", + "trigger_quick_create": "快速创建", + "trigger_direct": "直接指派", + "run_info": "运行详情", + "details_runtime": "运行时", + "details_provider": "提供方", + "details_mode": "模式", + "details_workdir": "工作目录", + "details_created": "创建", + "details_started": "开始", + "details_completed": "完成", + "copy_workdir": "复制工作目录" }, "task_failure": { "agent_error": "智能体执行出错",