Skip to content
Closed
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
70 changes: 37 additions & 33 deletions src/canvas/AssistantTracePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";

import { traceCategory } from "./traceCategory.ts";
import { useT } from "../i18n/I18nProvider";

import { normalizeDeepseekTrace } from "./deepseekTrace.ts";

Expand Down Expand Up @@ -31,27 +32,30 @@ type TraceRecord = {
};
};

const labels: Record<string, [string, string]> = {
system: ["系统", "System"], user: ["用户", "User"], context: ["上下文", "Context"],
assistant: ["助手", "Assistant"], tool: ["工具", "Tool"], model: ["模型", "Model"],
event: ["事件", "Event"], reasoning: ["思考", "Thinking"],
};
const labels = {
system: "assistant.trace.kind.system", user: "assistant.trace.kind.user",
context: "assistant.trace.kind.context", assistant: "assistant.trace.kind.assistant",
tool: "assistant.trace.kind.tool", model: "assistant.trace.kind.model",
event: "assistant.trace.kind.event", reasoning: "assistant.trace.kind.reasoning",
} as const;

function TraceRow({ kind, summary, value, chinese }: {
kind: string; summary: string; value: unknown; chinese: boolean;
function TraceRow({ kind, summary, value }: {
kind: keyof typeof labels; summary: string; value: unknown;
}) {
const t = useT();
return (
<details className="cw-trace-row">
<summary>
<span className={`cw-trace-badge is-${kind}`}>{labels[kind][chinese ? 0 : 1]}</span>
<span className={`cw-trace-badge is-${kind}`}>{t(labels[kind])}</span>
<span className="cw-trace-preview">{summary}</span>
</summary>
<pre>{typeof value === "string" ? value : JSON.stringify(value, null, 2)}</pre>
</details>
);
}

function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese: boolean; rawModel: boolean }) {
function TraceEntries({ traces, rawModel }: { traces: string[]; rawModel: boolean }) {
const t = useT();
const entries = useMemo(() => {
const parsed = traces.flatMap((trace, sourceIndex) => {
let timestamp = 0;
Expand Down Expand Up @@ -93,9 +97,9 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese
message.role === "user" && message.content === "Generate a title for this conversation:");
if (rawModel) {
request++;
const label = titleRequest ? (chinese ? "生成会话标题" : "Generate conversation title") :
(chinese ? `第 ${request} 次模型请求` : `Model request ${request}`);
return <TraceRow key={key} kind="model" summary={`${label} · ${payload.model ?? ""}`} value={record} chinese={chinese} />;
const label = titleRequest ? t("assistant.trace.titleRequest") :
t("assistant.trace.modelRequest", { count: request });
return <TraceRow key={key} kind="model" summary={`${label} · ${payload.model ?? ""}`} value={record} />;
}
const thoughts = (payload.messages ?? []).flatMap((message, index) => {
const text = message.reasoning_content?.trim();
Expand All @@ -104,7 +108,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese
if (seenReasoning.has(identity)) return [];
seenReasoning.add(identity);
if (hasStreamedReasoning || firstModelRequest || titleRequest) return [];
return [<TraceRow key={`reasoning:${index}`} kind="reasoning" summary={text} value={text} chinese={chinese} />];
return [<TraceRow key={`reasoning:${index}`} kind="reasoning" summary={text} value={text} />];
});
if (!titleRequest) firstModelRequest = false;
const inputs = titleRequest ? [] : (payload.messages ?? []).flatMap((message, index) => {
Expand All @@ -114,34 +118,34 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese
seenInputs.add(identity);
const content = typeof message.content === "string" ? message.content : identity;
return [<TraceRow key={index} kind="system"
summary={chinese ? "系统提示词" : "System prompt"}
value={content} chinese={chinese} />];
summary={t("assistant.trace.systemPrompt")}
value={content} />];
});
const toolsKey = payload.tools?.length ? JSON.stringify(payload.tools) : undefined;
if (toolsKey && !seenInputs.has(toolsKey)) {
seenInputs.add(toolsKey);
inputs.push(<TraceRow key="tools" kind="context"
summary={chinese ? `工具定义 · ${payload.tools?.length}` : `Tool definitions · ${payload.tools?.length}`}
value={payload.tools} chinese={chinese} />);
summary={t("assistant.trace.toolDefinitions", { count: payload.tools?.length ?? 0 })}
value={payload.tools} />);
}
return <div key={key}>{thoughts}{inputs}</div>;
}
const type = record?.native?.event_type ?? record?.native?.type ?? record?.event ?? record?.type;
const part = record?.native?.part;
if (type === "model.reasoning" && record?.text) {
return <TraceRow key={key} kind="reasoning" summary={record.text.trim()} value={record.text} chinese={chinese} />;
return <TraceRow key={key} kind="reasoning" summary={record.text.trim()} value={record.text} />;
}
if (!rawModel && type?.startsWith("model.")) return null;
if (type === "reasoning" && part?.text?.trim()) {
if (hasStreamedReasoning) return null;
return <TraceRow key={key} kind="reasoning" summary={part.text.trim()} value={part.text} chinese={chinese} />;
return <TraceRow key={key} kind="reasoning" summary={part.text.trim()} value={part.text} />;
}
const assistantText = type === "text" || type === "reasoning" ? part?.text :
type === "item.completed" && record?.native?.item?.type === "agent_message" ? record.native.item.text :
type === "assistant" ? record?.native?.message?.content
?.filter((block) => block.type === "text").map((block) => block.text).join("\n") : undefined;
if (assistantText?.trim()) {
return <TraceRow key={key} kind="assistant" summary={assistantText.trim()} value={assistantText} chinese={chinese} />;
return <TraceRow key={key} kind="assistant" summary={assistantText.trim()} value={assistantText} />;
}
if (type === "step_start" || type === "text" || type === "reasoning") return null;
const kind = traceCategory(
Expand All @@ -153,7 +157,7 @@ function TraceEntries({ traces, chinese, rawModel }: { traces: string[]; chinese
part?.state?.status ?? part?.reason ?? record?.error ?? record?.status,
record?.elapsedMs === undefined ? undefined : `${record.elapsedMs} ms`,
].filter(Boolean).join(" · ") || line;
return <TraceRow key={key} kind={kind} summary={summary} value={record ?? line} chinese={chinese} />;
return <TraceRow key={key} kind={kind} summary={summary} value={record ?? line} />;
});
}

Expand All @@ -167,7 +171,7 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams
const [showModel, setShowModel] = useState(false);
const rawModel = runtime === "opencode" && showModel;
const [status, setStatus] = useState<number | null>(null);
const chinese = navigator.language.startsWith("zh");
const t = useT();

useEffect(() => {
const controller = new AbortController();
Expand Down Expand Up @@ -213,31 +217,31 @@ export default function AssistantTracePage({ params }: { params: URLSearchParams
return (
<main className="cw-trace-page">
<header className="cw-trace-header">
<h1>{chinese ? "助手轨迹" : "Assistant trace"}</h1>
<h1>{t("assistant.trace.title")}</h1>
<p><code>{turn}</code></p>
{runtime === "opencode" && (
<nav className="cw-trace-tabs" aria-label={chinese ? "轨迹视图" : "Trace views"}>
<nav className="cw-trace-tabs" aria-label={t("assistant.trace.views")}>
<button type="button" aria-pressed={!rawModel} onClick={() => setShowModel(false)}>
{chinese ? "助手轨迹" : "Assistant trace"}
{t("assistant.trace.title")}
</button>
<button type="button" aria-pressed={rawModel} onClick={() => setShowModel(true)}>
{chinese ? "模型原始请求" : "Raw model requests"}
{t("assistant.trace.rawRequests")}
</button>
</nav>
)}
</header>
{!rawModel && input !== null && <TraceRow kind="user" summary={input} value={input} chinese={chinese} />}
{!rawModel && input !== null && <TraceRow kind="user" summary={input} value={input} />}
{status === 200 ? (
rawModel && !traces[0]?.trim() ? (
<p role="status">{chinese ? "该轮暂无模型请求记录。" : "No model requests recorded for this turn."}</p>
<p role="status">{t("assistant.trace.noModelRequests")}</p>
) : (
<TraceEntries traces={rawModel ? traces.slice(0, 1) : traces} chinese={chinese} rawModel={rawModel} />
<TraceEntries traces={rawModel ? traces.slice(0, 1) : traces} rawModel={rawModel} />
)
) : (
<p role="status">{status === null ? (chinese ? "加载中…" : "Loading…") :
status === 404 && live ? (chinese ? "等待首条轨迹…" : "Waiting for trace events…") :
status === 404 ? (chinese ? "该轮没有此类轨迹。" : "No trace of this type for this turn.") :
(chinese ? "轨迹加载失败。" : "Could not load trace.")}</p>
<p role="status">{status === null ? t("assistant.trace.loading") :
status === 404 && live ? t("assistant.trace.waiting") :
status === 404 ? t("assistant.trace.missing") :
t("assistant.trace.failed")}</p>
)}
</main>
);
Expand Down
5 changes: 4 additions & 1 deletion src/canvas/traceCategory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** Semantic colors are shared by every harness, independent of its event names. */
export function traceCategory(type?: string, role?: string): string {
export type TraceCategory =
| "system" | "user" | "context" | "reasoning" | "tool" | "assistant" | "model" | "event";

export function traceCategory(type?: string, role?: string): TraceCategory {
if (role === "system" || role === "developer" || type === "system") return "system";
if (role === "user" || type === "user" || type === "user_message") return "user";
if (type === "plan" || type === "context" || type === "session_meta" || type === "turn_context") return "context";
Expand Down
21 changes: 21 additions & 0 deletions src/i18n/messages/en-US/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ export const assistant = {
"assistant.panel.welcomeTitle": "Start from an idea",
"assistant.panel.welcomeLead": "Shape your next work with OpenDirector.",
"assistant.panel.trace": "Trace",
// Assistant trace page (opened from a reply's trace link).
"assistant.trace.title": "Assistant trace",
"assistant.trace.views": "Trace views",
"assistant.trace.rawRequests": "Raw model requests",
"assistant.trace.noModelRequests": "No model requests recorded for this turn.",
"assistant.trace.loading": "Loading…",
"assistant.trace.waiting": "Waiting for trace events…",
"assistant.trace.missing": "No trace of this type for this turn.",
"assistant.trace.failed": "Could not load trace.",
"assistant.trace.titleRequest": "Generate conversation title",
"assistant.trace.modelRequest": "Model request {count}",
"assistant.trace.systemPrompt": "System prompt",
"assistant.trace.toolDefinitions": "Tool definitions · {count}",
"assistant.trace.kind.system": "System",
"assistant.trace.kind.user": "User",
"assistant.trace.kind.context": "Context",
"assistant.trace.kind.assistant": "Assistant",
"assistant.trace.kind.tool": "Tool",
"assistant.trace.kind.model": "Model",
"assistant.trace.kind.event": "Event",
"assistant.trace.kind.reasoning": "Thinking",
"assistant.panel.welcomeBody":
"Describe a story, or type @ to add image, video, audio and text references.",
"assistant.panel.composer": "Chat input",
Expand Down
21 changes: 21 additions & 0 deletions src/i18n/messages/zh-CN/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ export const assistant = {
"assistant.panel.welcomeTitle": "从一个想法开始",
"assistant.panel.welcomeLead": "和 OpenDirector 一起构思你的下一部作品。",
"assistant.panel.trace": "轨迹",
// Assistant trace page (opened from a reply's trace link).
"assistant.trace.title": "助手轨迹",
"assistant.trace.views": "轨迹视图",
"assistant.trace.rawRequests": "模型原始请求",
"assistant.trace.noModelRequests": "该轮暂无模型请求记录。",
"assistant.trace.loading": "加载中…",
"assistant.trace.waiting": "等待首条轨迹…",
"assistant.trace.missing": "该轮没有此类轨迹。",
"assistant.trace.failed": "轨迹加载失败。",
"assistant.trace.titleRequest": "生成会话标题",
"assistant.trace.modelRequest": "第 {count} 次模型请求",
"assistant.trace.systemPrompt": "系统提示词",
"assistant.trace.toolDefinitions": "工具定义 · {count}",
"assistant.trace.kind.system": "系统",
"assistant.trace.kind.user": "用户",
"assistant.trace.kind.context": "上下文",
"assistant.trace.kind.assistant": "助手",
"assistant.trace.kind.tool": "工具",
"assistant.trace.kind.model": "模型",
"assistant.trace.kind.event": "事件",
"assistant.trace.kind.reasoning": "思考",
"assistant.panel.welcomeBody":
"描述故事,或输入 @ 添加图片、视频、音频与文本参考。",
"assistant.panel.composer": "聊天输入框",
Expand Down