@keyou007/agent-bot 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +15 -5
- package/README.zh.md +15 -5
- package/dist/codex/CodexEventMapper.js +21 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +3 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +136 -4
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderModels.js +159 -0
- package/dist/codex/ProviderModels.js.map +1 -0
- package/dist/commands/CommandRouter.js +8 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +46 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +34 -8
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +151 -16
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +725 -0
- package/dist/local-files/TurnPreviewPage.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +6 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +142 -3
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +186 -80
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +7 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/formatStorageSize.js +15 -0
- package/dist/utils/formatStorageSize.js.map +1 -0
- package/docs/technical-reference.md +7 -1
- package/docs/technical-reference.zh.md +7 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +5 -1
- package/src/codex/CodexEventMapper.ts +31 -1
- package/src/codex/CodexProcessManager.ts +4 -0
- package/src/codex/CodexRuntime.ts +153 -9
- package/src/codex/ProviderModels.ts +185 -0
- package/src/commands/CommandRouter.ts +7 -2
- package/src/commands/commandTypes.ts +1 -1
- package/src/console/ConsoleTurnPresenter.ts +3 -0
- package/src/feishu/CardRenderer.ts +47 -16
- package/src/feishu/FeishuTurnPresenter.ts +46 -4
- package/src/index.ts +2 -0
- package/src/local-files/LocalFileViewerServer.ts +166 -17
- package/src/local-files/TurnPreviewPage.ts +792 -0
- package/src/presentation/OutboundRouter.ts +8 -0
- package/src/presentation/TurnStateReducer.ts +175 -2
- package/src/presentation/turnViewTypes.ts +13 -0
- package/src/proxy/ProxySessionController.ts +186 -67
- package/src/runtime/types.ts +18 -1
- package/src/state/StateStore.ts +9 -0
- package/src/utils/formatStorageSize.ts +13 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
import type { ToolState } from "../runtime/types.js";
|
|
4
|
+
import type { FileSummary, TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
5
|
+
import { displayToolCommand, formatShellCommandForDisplay } from "../feishu/CardRenderer.js";
|
|
6
|
+
|
|
7
|
+
const MARKDOWN = new MarkdownIt({
|
|
8
|
+
breaks: true,
|
|
9
|
+
html: false,
|
|
10
|
+
linkify: true,
|
|
11
|
+
typographer: false,
|
|
12
|
+
});
|
|
13
|
+
const defaultLinkOpen = MARKDOWN.renderer.rules.link_open
|
|
14
|
+
?? ((tokens, index, options, _environment, renderer) => renderer.renderToken(tokens, index, options));
|
|
15
|
+
MARKDOWN.renderer.rules.link_open = (tokens, index, options, environment, renderer) => {
|
|
16
|
+
tokens[index]?.attrSet("target", "_blank");
|
|
17
|
+
tokens[index]?.attrSet("rel", "noreferrer noopener");
|
|
18
|
+
return defaultLinkOpen(tokens, index, options, environment, renderer);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TurnPreviewLanguage = "zh" | "en";
|
|
22
|
+
|
|
23
|
+
const PREVIEW_LABELS = {
|
|
24
|
+
zh: {
|
|
25
|
+
live: "实时更新",
|
|
26
|
+
waitingForUpdates: "等待更新",
|
|
27
|
+
unavailable: "暂时不可用",
|
|
28
|
+
reconnecting: "正在重连",
|
|
29
|
+
elapsed: "耗时 ",
|
|
30
|
+
unknown: "未知",
|
|
31
|
+
plan: "计划",
|
|
32
|
+
error: "错误",
|
|
33
|
+
finalAnswer: "最终回答",
|
|
34
|
+
generating: "回答生成中",
|
|
35
|
+
command: "命令",
|
|
36
|
+
result: "结果",
|
|
37
|
+
output: "输出",
|
|
38
|
+
resultError: "结果 / 错误",
|
|
39
|
+
files: "文件",
|
|
40
|
+
startingTime: "开始时间",
|
|
41
|
+
noToolDetails: "没有可显示的工具详情。",
|
|
42
|
+
fileChanges: "文件变更",
|
|
43
|
+
model: "模型",
|
|
44
|
+
tools: "个工具",
|
|
45
|
+
turnTokens: "本轮",
|
|
46
|
+
compactionTokens: "压缩",
|
|
47
|
+
compactionAfterTokens: "压缩后",
|
|
48
|
+
characters: "字符",
|
|
49
|
+
truncated: "较早的活动已被运行时截断;本页展示 Agent Bot 当前保存的完整快照。",
|
|
50
|
+
statuses: {
|
|
51
|
+
starting: "正在启动",
|
|
52
|
+
running: "正在处理",
|
|
53
|
+
tool_running: "正在执行工具",
|
|
54
|
+
waiting_for_approval: "等待确认",
|
|
55
|
+
completed: "已完成",
|
|
56
|
+
cancelled: "已停止",
|
|
57
|
+
failed: "执行失败",
|
|
58
|
+
},
|
|
59
|
+
toolStatuses: { completed: "成功", failed: "失败", running: "进行中" },
|
|
60
|
+
toolImage: "工具图片",
|
|
61
|
+
},
|
|
62
|
+
en: {
|
|
63
|
+
live: "Live updates",
|
|
64
|
+
waitingForUpdates: "Waiting for updates",
|
|
65
|
+
unavailable: "Temporarily unavailable",
|
|
66
|
+
reconnecting: "Reconnecting",
|
|
67
|
+
elapsed: "Duration ",
|
|
68
|
+
unknown: "Unknown",
|
|
69
|
+
plan: "Plan",
|
|
70
|
+
error: "Error",
|
|
71
|
+
finalAnswer: "Final answer",
|
|
72
|
+
generating: "Generating answer",
|
|
73
|
+
command: "Command",
|
|
74
|
+
result: "Result",
|
|
75
|
+
output: "Output",
|
|
76
|
+
resultError: "Result / error",
|
|
77
|
+
files: "Files",
|
|
78
|
+
startingTime: "Start time",
|
|
79
|
+
noToolDetails: "No tool details available.",
|
|
80
|
+
fileChanges: "File changes",
|
|
81
|
+
model: "Model",
|
|
82
|
+
tools: "tools",
|
|
83
|
+
turnTokens: "Turn",
|
|
84
|
+
compactionTokens: "Compaction",
|
|
85
|
+
compactionAfterTokens: "After compaction",
|
|
86
|
+
characters: "characters",
|
|
87
|
+
truncated: "Earlier activities were truncated by the runtime; this page shows the complete snapshot currently saved by Agent Bot.",
|
|
88
|
+
statuses: {
|
|
89
|
+
starting: "Starting",
|
|
90
|
+
running: "Processing",
|
|
91
|
+
tool_running: "Running tool",
|
|
92
|
+
waiting_for_approval: "Waiting for approval",
|
|
93
|
+
completed: "Completed",
|
|
94
|
+
cancelled: "Stopped",
|
|
95
|
+
failed: "Failed",
|
|
96
|
+
},
|
|
97
|
+
toolStatuses: { completed: "Success", failed: "Failed", running: "In progress" },
|
|
98
|
+
toolImage: "Tool image",
|
|
99
|
+
},
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
function previewLabels(language: TurnPreviewLanguage) {
|
|
103
|
+
return PREVIEW_LABELS[language];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function detectTurnPreviewLanguage(
|
|
107
|
+
acceptLanguage: string | string[] | undefined,
|
|
108
|
+
): TurnPreviewLanguage {
|
|
109
|
+
const value = Array.isArray(acceptLanguage) ? acceptLanguage.join(",") : acceptLanguage;
|
|
110
|
+
if (!value?.trim()) return "zh";
|
|
111
|
+
const candidates = value.split(",")
|
|
112
|
+
.map((item, index) => {
|
|
113
|
+
const [tag, ...parameters] = item.trim().split(";");
|
|
114
|
+
const quality = Number(parameters.find((parameter) => parameter.trim().startsWith("q="))?.trim().slice(2) ?? "1");
|
|
115
|
+
return { tag: tag?.trim() ?? "", quality: Number.isFinite(quality) ? quality : 0, index };
|
|
116
|
+
})
|
|
117
|
+
.filter((item) => item.tag && item.quality > 0)
|
|
118
|
+
.sort((left, right) => right.quality - left.quality || left.index - right.index);
|
|
119
|
+
return /^zh(?:[-_]|$)/iu.test(candidates[0]?.tag ?? "") ? "zh" : "en";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const TURN_PREVIEW_CLIENT_SCRIPT = `(() => {
|
|
123
|
+
const eventsUrl = document.body.dataset.eventsUrl;
|
|
124
|
+
const content = document.getElementById("turn-content");
|
|
125
|
+
const metadata = document.getElementById("turn-metadata");
|
|
126
|
+
const status = document.getElementById("turn-status");
|
|
127
|
+
const live = document.getElementById("turn-live");
|
|
128
|
+
if (!eventsUrl || !content || !metadata || !status || !live || typeof EventSource !== "function") return;
|
|
129
|
+
|
|
130
|
+
const isChinese = /^zh/i.test(document.documentElement.lang);
|
|
131
|
+
const labels = {
|
|
132
|
+
elapsed: isChinese ? "耗时 " : "Duration ",
|
|
133
|
+
live: isChinese ? "实时更新" : "Live updates",
|
|
134
|
+
waitingForUpdates: isChinese ? "等待更新" : "Waiting for updates",
|
|
135
|
+
unavailable: isChinese ? "暂时不可用" : "Temporarily unavailable",
|
|
136
|
+
reconnecting: isChinese ? "正在重连" : "Reconnecting",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let source;
|
|
140
|
+
const updateElapsed = () => {
|
|
141
|
+
document.querySelectorAll("[data-live-elapsed]").forEach((element) => {
|
|
142
|
+
const startedAt = Number(element.dataset.startedAt);
|
|
143
|
+
const completedAt = Number(element.dataset.completedAt) || Date.now();
|
|
144
|
+
if (!Number.isFinite(startedAt)) return;
|
|
145
|
+
const seconds = Math.max(0, Math.floor((completedAt - startedAt) / 1000));
|
|
146
|
+
const hours = Math.floor(seconds / 3600);
|
|
147
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
148
|
+
const remainder = seconds % 60;
|
|
149
|
+
element.textContent = hours > 0
|
|
150
|
+
? String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0")
|
|
151
|
+
: String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0");
|
|
152
|
+
});
|
|
153
|
+
document.querySelectorAll("[data-live-tool-duration]").forEach((element) => {
|
|
154
|
+
const startedAt = Number(element.dataset.startedAt);
|
|
155
|
+
const completedAt = Number(element.dataset.completedAt) || Date.now();
|
|
156
|
+
if (!Number.isFinite(startedAt)) return;
|
|
157
|
+
const seconds = Math.max(0, Math.floor((completedAt - startedAt) / 1000));
|
|
158
|
+
const hours = Math.floor(seconds / 3600);
|
|
159
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
160
|
+
const remainder = seconds % 60;
|
|
161
|
+
const duration = hours > 0
|
|
162
|
+
? String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0")
|
|
163
|
+
: String(minutes).padStart(2, "0") + ":" + String(remainder).padStart(2, "0");
|
|
164
|
+
element.textContent = labels.elapsed + duration;
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
updateElapsed();
|
|
168
|
+
setInterval(updateElapsed, 1000);
|
|
169
|
+
|
|
170
|
+
const replaceSnapshot = (update) => {
|
|
171
|
+
const disclosures = new Map(Array.from(content.querySelectorAll("details[data-activity-id]"), (item) => [item.dataset.activityId, item.open]));
|
|
172
|
+
const scrollPositions = new Map(Array.from(content.querySelectorAll("[data-scroll-id]"), (item) => [item.dataset.scrollId, [item.scrollLeft, item.scrollTop]]));
|
|
173
|
+
const top = window.scrollY;
|
|
174
|
+
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
175
|
+
const atBottom = maxTop - top <= 32;
|
|
176
|
+
content.innerHTML = update.content;
|
|
177
|
+
metadata.innerHTML = update.metadata;
|
|
178
|
+
status.textContent = update.statusLabel;
|
|
179
|
+
status.className = "status " + update.status;
|
|
180
|
+
for (const item of content.querySelectorAll("details[data-activity-id]")) {
|
|
181
|
+
if (disclosures.has(item.dataset.activityId)) item.open = disclosures.get(item.dataset.activityId);
|
|
182
|
+
}
|
|
183
|
+
for (const item of content.querySelectorAll("[data-scroll-id]")) {
|
|
184
|
+
const position = scrollPositions.get(item.dataset.scrollId);
|
|
185
|
+
if (position) { item.scrollLeft = position[0]; item.scrollTop = position[1]; }
|
|
186
|
+
}
|
|
187
|
+
updateElapsed();
|
|
188
|
+
requestAnimationFrame(() => {
|
|
189
|
+
const nextMaxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
190
|
+
window.scrollTo(0, atBottom ? nextMaxTop : Math.min(top, nextMaxTop));
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
source = new EventSource(eventsUrl);
|
|
195
|
+
source.addEventListener("update", (event) => {
|
|
196
|
+
try {
|
|
197
|
+
const update = JSON.parse(event.data);
|
|
198
|
+
if (typeof update.content !== "string" || typeof update.metadata !== "string") return;
|
|
199
|
+
replaceSnapshot(update);
|
|
200
|
+
live.textContent = update.terminal ? "" : labels.live;
|
|
201
|
+
live.className = update.terminal ? "live terminal" : "live connected";
|
|
202
|
+
if (update.terminal) source.close();
|
|
203
|
+
} catch {
|
|
204
|
+
live.textContent = labels.waitingForUpdates;
|
|
205
|
+
live.className = "live";
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
source.addEventListener("unavailable", () => {
|
|
209
|
+
live.textContent = labels.unavailable;
|
|
210
|
+
live.className = "live disconnected";
|
|
211
|
+
});
|
|
212
|
+
source.onerror = () => {
|
|
213
|
+
live.textContent = labels.reconnecting;
|
|
214
|
+
live.className = "live disconnected";
|
|
215
|
+
};
|
|
216
|
+
window.addEventListener("pagehide", () => source.close(), { once: true });
|
|
217
|
+
})();
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
export interface TurnPreviewSnapshot {
|
|
221
|
+
content: string;
|
|
222
|
+
metadata: string;
|
|
223
|
+
status: TurnViewStatus;
|
|
224
|
+
statusLabel: string;
|
|
225
|
+
terminal: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function isTurnPreviewState(value: unknown): value is TurnViewState {
|
|
229
|
+
if (!value || typeof value !== "object") return false;
|
|
230
|
+
const state = value as Partial<TurnViewState>;
|
|
231
|
+
return typeof state.sessionId === "string"
|
|
232
|
+
&& typeof state.turnId === "string"
|
|
233
|
+
&& typeof state.startedAt === "number"
|
|
234
|
+
&& typeof state.status === "string"
|
|
235
|
+
&& Array.isArray(state.plan)
|
|
236
|
+
&& Array.isArray(state.completedTools)
|
|
237
|
+
&& Array.isArray(state.failedTools)
|
|
238
|
+
&& Array.isArray(state.fileSummary);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function renderTurnPreviewPage(input: {
|
|
242
|
+
state: TurnViewState;
|
|
243
|
+
eventsUrl: string;
|
|
244
|
+
scriptPath: string;
|
|
245
|
+
localFileUrl?: (filePath: string) => string | undefined;
|
|
246
|
+
language?: TurnPreviewLanguage;
|
|
247
|
+
}): string {
|
|
248
|
+
const language = input.language ?? "zh";
|
|
249
|
+
const snapshot = renderTurnPreviewSnapshot(input.state, input.localFileUrl, language);
|
|
250
|
+
const title = previewTitle(input.state);
|
|
251
|
+
return `<!doctype html>
|
|
252
|
+
<html lang="${language === "zh" ? "zh-CN" : "en"}">
|
|
253
|
+
<head>
|
|
254
|
+
<meta charset="utf-8">
|
|
255
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
256
|
+
<title>${escapeHtml(title)} · Agent Bot</title>
|
|
257
|
+
<style>${TURN_PREVIEW_CSS}</style>
|
|
258
|
+
</head>
|
|
259
|
+
<body data-events-url="${escapeAttribute(input.eventsUrl)}">
|
|
260
|
+
<header class="page-header">
|
|
261
|
+
<div class="header-inner">
|
|
262
|
+
<div class="header-top">
|
|
263
|
+
<h1 title="Turn ${escapeAttribute(input.state.turnId)}">Turn Preview</h1>
|
|
264
|
+
<span id="turn-status" class="status ${snapshot.status}" role="status">${escapeHtml(snapshot.statusLabel)}</span>
|
|
265
|
+
<span id="turn-live" class="live ${snapshot.terminal ? "terminal" : "connected"}" role="status">${snapshot.terminal ? "" : escapeHtml(previewLabels(language).live)}</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div id="turn-metadata" class="metadata">${snapshot.metadata}</div>
|
|
268
|
+
</div>
|
|
269
|
+
</header>
|
|
270
|
+
<main id="turn-content">${snapshot.content}</main>
|
|
271
|
+
<script src="${escapeAttribute(input.scriptPath)}" defer></script>
|
|
272
|
+
</body>
|
|
273
|
+
</html>`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function renderTurnPreviewSnapshot(
|
|
277
|
+
state: TurnViewState,
|
|
278
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
279
|
+
language: TurnPreviewLanguage = "zh",
|
|
280
|
+
): TurnPreviewSnapshot {
|
|
281
|
+
const labels = previewLabels(language);
|
|
282
|
+
const resolveFileUrl = localFileUrl ? (filePath: string): string | undefined => {
|
|
283
|
+
if (path.isAbsolute(filePath) || path.win32.isAbsolute(filePath)) return localFileUrl(filePath);
|
|
284
|
+
if (!state.projectCwd || !path.isAbsolute(state.projectCwd)) return undefined;
|
|
285
|
+
return localFileUrl(path.resolve(state.projectCwd, filePath));
|
|
286
|
+
} : undefined;
|
|
287
|
+
const timeline = renderTimeline(state.activities ?? [], resolveFileUrl, state.fullToolOutputs, state.fullToolErrors, language);
|
|
288
|
+
const sections = [
|
|
289
|
+
state.prompt ? renderMessageActivity("prompt", state.prompt, "user prompt") : "",
|
|
290
|
+
state.plan.length > 0 ? renderPlan(state, language) : "",
|
|
291
|
+
state.activitiesTruncated
|
|
292
|
+
? `<div class="notice">${escapeHtml(labels.truncated)}</div>`
|
|
293
|
+
: "",
|
|
294
|
+
timeline ? `<section class="timeline">${timeline}</section>` : `<div class="empty">${escapeHtml(language === "zh" ? "正在等待 Agent 返回进度…" : "Waiting for Agent progress…")}</div>`,
|
|
295
|
+
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
|
|
296
|
+
state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
297
|
+
state.finalResponse
|
|
298
|
+
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderMarkdown(state.finalResponse)}</div></section>`
|
|
299
|
+
: state.assistantText
|
|
300
|
+
? `<section class="result"><h2>${escapeHtml(labels.generating)}</h2><div class="markdown">${renderMarkdown(state.assistantText)}</div></section>`
|
|
301
|
+
: "",
|
|
302
|
+
].filter(Boolean).join("");
|
|
303
|
+
const terminal = isTerminal(state.status);
|
|
304
|
+
return {
|
|
305
|
+
content: sections,
|
|
306
|
+
metadata: renderMetadata(state, language),
|
|
307
|
+
status: state.status,
|
|
308
|
+
statusLabel: statusLabel(state.status, language),
|
|
309
|
+
terminal,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderPlan(state: TurnViewState, language: TurnPreviewLanguage): string {
|
|
314
|
+
const labels = previewLabels(language);
|
|
315
|
+
const completed = state.plan.filter((step) => step.status === "completed").length;
|
|
316
|
+
const items = state.plan.map((step) => {
|
|
317
|
+
const marker = step.status === "completed" ? "✓" : step.status === "in_progress" ? "↻" : "○";
|
|
318
|
+
return `<li class="${step.status}"><span>${marker}</span><span>${escapeHtml(step.text)}</span></li>`;
|
|
319
|
+
}).join("");
|
|
320
|
+
return `<section class="plan"><h2>${escapeHtml(labels.plan)} <span>${completed}/${state.plan.length}</span></h2><ol>${items}</ol></section>`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderTimeline(
|
|
324
|
+
activities: TurnActivity[],
|
|
325
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
326
|
+
fullToolOutputs?: Record<string, string>,
|
|
327
|
+
fullToolErrors?: Record<string, string>,
|
|
328
|
+
language: TurnPreviewLanguage = "zh",
|
|
329
|
+
): string {
|
|
330
|
+
return activities
|
|
331
|
+
.filter((activity): activity is Exclude<TurnActivity, { kind: "reasoning" }> => activity.kind !== "reasoning")
|
|
332
|
+
.map((activity) => renderActivity(activity, localFileUrl, fullToolOutputs, fullToolErrors, language))
|
|
333
|
+
.join("");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderActivity(
|
|
337
|
+
activity: Exclude<TurnActivity, { kind: "reasoning" }>,
|
|
338
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
339
|
+
fullToolOutputs?: Record<string, string>,
|
|
340
|
+
fullToolErrors?: Record<string, string>,
|
|
341
|
+
language: TurnPreviewLanguage = "zh",
|
|
342
|
+
): string {
|
|
343
|
+
switch (activity.kind) {
|
|
344
|
+
case "assistant":
|
|
345
|
+
return renderMessageActivity(activity.id, activity.text, "commentary");
|
|
346
|
+
case "user":
|
|
347
|
+
return renderMessageActivity(activity.id, activity.text, "user");
|
|
348
|
+
case "tool":
|
|
349
|
+
return renderToolActivity(activity.id, activity.tool, localFileUrl, fullToolOutputs, fullToolErrors, language);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function renderMessageActivity(id: string, text: string, kind: string): string {
|
|
354
|
+
return `<article class="activity message ${kind}" data-activity="${escapeAttribute(id)}"><div class="markdown">${renderMarkdown(text)}</div></article>`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderToolActivity(
|
|
358
|
+
activityId: string,
|
|
359
|
+
tool: ToolState,
|
|
360
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
361
|
+
fullToolOutputs?: Record<string, string>,
|
|
362
|
+
fullToolErrors?: Record<string, string>,
|
|
363
|
+
language: TurnPreviewLanguage = "zh",
|
|
364
|
+
): string {
|
|
365
|
+
const labels = previewLabels(language);
|
|
366
|
+
const command = tool.command ? (displayToolCommand(tool.command) || tool.command.trim()) : undefined;
|
|
367
|
+
const repl = isReplTool(tool);
|
|
368
|
+
const displayCommand = repl
|
|
369
|
+
? formatReplCommand(command ?? "")
|
|
370
|
+
: command
|
|
371
|
+
? formatToolCommand(command)
|
|
372
|
+
: undefined;
|
|
373
|
+
const commandInHeader = Boolean(displayCommand);
|
|
374
|
+
const title = commandInHeader ? displayCommand! : toolTitle(tool, command, repl, language);
|
|
375
|
+
const output = fullToolOutputs?.[tool.id] ?? tool.output;
|
|
376
|
+
const error = fullToolErrors?.[tool.id] ?? tool.error;
|
|
377
|
+
const sameError = Boolean(output && error && normalizeOutput(output) === normalizeOutput(error));
|
|
378
|
+
const detailParts = [
|
|
379
|
+
displayCommand && !commandInHeader ? `<pre class="command-block${repl ? " repl-code" : ""}" data-scroll-id="${escapeAttribute(`${activityId}:command`)}">${escapeHtml(displayCommand)}</pre>` : "",
|
|
380
|
+
output
|
|
381
|
+
? renderToolOutput(activityId, "output", sameError ? labels.resultError : repl ? labels.result : labels.output, output, sameError, repl, language, tool)
|
|
382
|
+
: "",
|
|
383
|
+
error && !sameError
|
|
384
|
+
? renderToolOutput(activityId, "error", labels.error, error, true, repl, language, tool)
|
|
385
|
+
: "",
|
|
386
|
+
!output && !error && !tool.files?.length ? renderToolStatusRow(tool, language) : "",
|
|
387
|
+
renderToolImage(tool, localFileUrl, language),
|
|
388
|
+
tool.files?.length
|
|
389
|
+
? `<details class="tool-output" data-activity-id="${escapeAttribute(`${activityId}:files`)}"><summary><span class="tool-output-label">${escapeHtml(labels.files)}</span><span class="tool-output-meta">${renderToolTiming(tool, language)}<span class="tool-state ${tool.status}" aria-label="${escapeAttribute(tool.status)}">${toolStatusIcon(tool.status, language)}</span><span>${tool.files.length}</span></span></summary><ul class="files">${tool.files.map((file) => renderFileEntry(file, localFileUrl)).join("")}</ul></details>`
|
|
390
|
+
: "",
|
|
391
|
+
].filter(Boolean).join("");
|
|
392
|
+
const titleMarkup = commandInHeader
|
|
393
|
+
? `<code class="tool-command-title">${escapeHtml(title)}</code>`
|
|
394
|
+
: `<span class="tool-title">${escapeHtml(title)}</span>`;
|
|
395
|
+
return `<article class="activity tool"><section class="tool-step" data-activity-id="${escapeAttribute(activityId)}"><div class="tool-header" data-scroll-id="${escapeAttribute(`${activityId}:command`)}">${titleMarkup}</div><div class="tool-body">${detailParts || `<div class="muted">${escapeHtml(labels.noToolDetails)}</div>`}</div></section></article>`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function toolTitle(tool: ToolState, command?: string, repl = false, language: TurnPreviewLanguage = "zh"): string {
|
|
399
|
+
if (repl) return "REPL";
|
|
400
|
+
if (command) return tool.kind === "command" ? previewLabels(language).command : tool.kind;
|
|
401
|
+
const title = tool.title.trim();
|
|
402
|
+
return title || tool.kind;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isReplTool(tool: ToolState): boolean {
|
|
406
|
+
const title = tool.title.trim();
|
|
407
|
+
const titleLooksLikeToolName = title.length <= 120 && !/[\s"'`]/u.test(title);
|
|
408
|
+
return tool.kind.toLowerCase().includes("repl")
|
|
409
|
+
|| (titleLooksLikeToolName && /(?:^|[._-])repl(?:$|[._-])/iu.test(title));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function formatReplCommand(command: string): string {
|
|
413
|
+
const normalized = command.trim();
|
|
414
|
+
const newline = normalized.indexOf("\n");
|
|
415
|
+
if (newline >= 0) {
|
|
416
|
+
const payload = normalized.slice(newline + 1).trim();
|
|
417
|
+
try {
|
|
418
|
+
const parsed: unknown = JSON.parse(payload);
|
|
419
|
+
if (parsed && typeof parsed === "object") {
|
|
420
|
+
for (const key of ["input", "code", "script", "javascript", "source"]) {
|
|
421
|
+
const value = (parsed as Record<string, unknown>)[key];
|
|
422
|
+
if (typeof value === "string" && value.trim()) return formatReplSource(value);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Keep the original payload when a provider uses a non-JSON argument format.
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return formatReplSource(newline >= 0 ? normalized.slice(0, newline) : normalized);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function formatToolCommand(command: string): string {
|
|
433
|
+
const normalized = command.replace(/\r\n?/gu, "\n").trim();
|
|
434
|
+
const newline = normalized.indexOf("\n");
|
|
435
|
+
if (newline >= 0) {
|
|
436
|
+
const name = normalized.slice(0, newline).trim();
|
|
437
|
+
const payload = normalized.slice(newline + 1).trim();
|
|
438
|
+
const formattedPayload = formatJsonPayload(payload);
|
|
439
|
+
if (name && formattedPayload) return `${name}\n${formattedPayload}`;
|
|
440
|
+
}
|
|
441
|
+
return formatShellCommandForDisplay(normalized);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatJsonPayload(value: string): string | undefined {
|
|
445
|
+
try {
|
|
446
|
+
const parsed: unknown = JSON.parse(value);
|
|
447
|
+
if (parsed && typeof parsed === "object") return JSON.stringify(parsed, null, 2);
|
|
448
|
+
} catch {
|
|
449
|
+
// Keep non-JSON tool arguments in their original readable form.
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function formatReplSource(source: string): string {
|
|
455
|
+
const normalized = source.replace(/\r\n?/gu, "\n").trim();
|
|
456
|
+
if (normalized.includes("\n")) return normalized;
|
|
457
|
+
const lines: string[] = [];
|
|
458
|
+
let current = "";
|
|
459
|
+
let quote: "'" | "\"" | "`" | undefined;
|
|
460
|
+
let escaped = false;
|
|
461
|
+
for (const character of normalized) {
|
|
462
|
+
current += character;
|
|
463
|
+
if (escaped) { escaped = false; continue; }
|
|
464
|
+
if (character === "\\" && quote) { escaped = true; continue; }
|
|
465
|
+
if (quote) { if (character === quote) quote = undefined; continue; }
|
|
466
|
+
if (character === "'" || character === "\"" || character === "`") { quote = character; continue; }
|
|
467
|
+
if (character === ";") {
|
|
468
|
+
lines.push(current.trim());
|
|
469
|
+
current = "";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (current.trim()) lines.push(current.trim());
|
|
473
|
+
return lines.join("\n");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function normalizeOutput(value: string): string {
|
|
477
|
+
return value.replace(/\r\n?/gu, "\n").trim();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function formatReplOutput(value: string): string {
|
|
481
|
+
const normalized = normalizeOutput(value);
|
|
482
|
+
if (!normalized) return normalized;
|
|
483
|
+
try {
|
|
484
|
+
const parsed: unknown = JSON.parse(normalized);
|
|
485
|
+
const text = extractReplOutputText(parsed);
|
|
486
|
+
if (text) return text;
|
|
487
|
+
} catch {
|
|
488
|
+
// REPL providers may return plain text instead of JSON.
|
|
489
|
+
}
|
|
490
|
+
return normalizeReplText(normalized);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function extractReplOutputText(value: unknown): string | undefined {
|
|
494
|
+
if (!value || typeof value !== "object") return undefined;
|
|
495
|
+
const record = value as Record<string, unknown>;
|
|
496
|
+
const content = record.content;
|
|
497
|
+
if (!Array.isArray(content)) return undefined;
|
|
498
|
+
const texts = content.flatMap((item): string[] => {
|
|
499
|
+
if (!item || typeof item !== "object") return [];
|
|
500
|
+
const text = (item as Record<string, unknown>).text;
|
|
501
|
+
return typeof text === "string" && text.trim() ? [normalizeReplText(text)] : [];
|
|
502
|
+
});
|
|
503
|
+
return texts.length > 0 ? texts.join("\n\n") : undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function normalizeReplText(value: string): string {
|
|
507
|
+
return value
|
|
508
|
+
.replaceAll("\\n", "\n")
|
|
509
|
+
.replaceAll("\\r", "\r")
|
|
510
|
+
.replace(/\r\n?/gu, "\n")
|
|
511
|
+
.trim();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderToolOutput(
|
|
515
|
+
activityId: string,
|
|
516
|
+
kind: string,
|
|
517
|
+
label: string,
|
|
518
|
+
value: string,
|
|
519
|
+
error = false,
|
|
520
|
+
repl = false,
|
|
521
|
+
language: TurnPreviewLanguage = "zh",
|
|
522
|
+
tool?: ToolState,
|
|
523
|
+
): string {
|
|
524
|
+
const id = escapeAttribute(`${activityId}:${kind}`);
|
|
525
|
+
const displayValue = repl && !error ? formatReplOutput(value) : value;
|
|
526
|
+
const labels = previewLabels(language);
|
|
527
|
+
const status = tool?.status ?? "completed";
|
|
528
|
+
const timing = tool ? renderToolTiming(tool, language) : "";
|
|
529
|
+
return `<details class="tool-output${error ? " error-output" : ""}${repl ? " repl-result" : ""}" data-activity-id="${id}"><summary><span class="tool-output-label">${escapeHtml(label)}</span><span class="tool-output-meta">${timing}<span class="tool-state ${status}" aria-label="${escapeAttribute(status)}">${toolStatusIcon(status, language)}</span><span>${formatNumber(displayValue.length)} ${escapeHtml(labels.characters)}</span></span></summary><pre data-scroll-id="${id}">${escapeHtml(displayValue)}</pre></details>`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function renderToolStatusRow(tool: ToolState, language: TurnPreviewLanguage): string {
|
|
533
|
+
const timing = renderToolTiming(tool, language);
|
|
534
|
+
return `<div class="tool-status-row"><span class="tool-output-meta">${timing}<span class="tool-state ${tool.status}" aria-label="${escapeAttribute(tool.status)}">${toolStatusIcon(tool.status, language)}</span></span></div>`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function renderToolTiming(tool: ToolState, language: TurnPreviewLanguage): string {
|
|
538
|
+
const labels = previewLabels(language);
|
|
539
|
+
const start = tool.startedAt === undefined ? labels.unknown : formatTimestamp(tool.startedAt);
|
|
540
|
+
const duration = toolDuration(tool);
|
|
541
|
+
const liveDuration = duration === undefined
|
|
542
|
+
? `<span>${escapeHtml(labels.elapsed.trim())}${language === "zh" ? "" : " "}${escapeHtml(labels.unknown)}</span>`
|
|
543
|
+
: `<span${tool.status === "running" && tool.completedAt === undefined ? ` data-live-tool-duration data-started-at="${tool.startedAt}"` : ""}>${escapeHtml(labels.elapsed)}${escapeHtml(duration)}</span>`;
|
|
544
|
+
return `<span title="${escapeAttribute(labels.startingTime)}">${escapeHtml(start)}</span>${liveDuration}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderToolImage(
|
|
548
|
+
tool: ToolState,
|
|
549
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
550
|
+
language: TurnPreviewLanguage = "zh",
|
|
551
|
+
): string {
|
|
552
|
+
if (!tool.imagePath || !localFileUrl) return "";
|
|
553
|
+
const viewerUrl = localFileUrl(tool.imagePath);
|
|
554
|
+
if (!viewerUrl) return "";
|
|
555
|
+
try {
|
|
556
|
+
const rawUrl = new URL(viewerUrl);
|
|
557
|
+
rawUrl.searchParams.set("raw", "1");
|
|
558
|
+
return `<div class="tool-image"><img src="${escapeAttribute(rawUrl.toString())}" alt="${escapeAttribute(tool.title || previewLabels(language).toolImage)}"></div>`;
|
|
559
|
+
} catch {
|
|
560
|
+
return "";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function renderFileSummary(
|
|
565
|
+
state: TurnViewState,
|
|
566
|
+
language: TurnPreviewLanguage,
|
|
567
|
+
localFileUrl?: (filePath: string) => string | undefined,
|
|
568
|
+
): string {
|
|
569
|
+
const labels = previewLabels(language);
|
|
570
|
+
const files = state.fileSummary.map((file) => renderFileEntry(file, localFileUrl)).join("");
|
|
571
|
+
return `<section class="files-summary"><h2>${escapeHtml(labels.fileChanges)} <span>${state.fileSummary.length}</span></h2><ul class="files">${files}</ul></section>`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderFileEntry(file: FileSummary, localFileUrl?: (filePath: string) => string | undefined): string {
|
|
575
|
+
const label = `<code>${escapeHtml(file.path)}</code>`;
|
|
576
|
+
const url = localFileUrl?.(file.path);
|
|
577
|
+
const link = url
|
|
578
|
+
? `<a class="file-link" href="${escapeAttribute(url)}" target="_blank" rel="noreferrer noopener">${label}</a>`
|
|
579
|
+
: label;
|
|
580
|
+
return `<li>${link}${fileDelta(file)}</li>`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function fileDelta(file: { additions?: number; deletions?: number }): string {
|
|
584
|
+
const additions = file.additions ? `<span class="additions">+${file.additions}</span>` : "";
|
|
585
|
+
const deletions = file.deletions ? `<span class="deletions">-${file.deletions}</span>` : "";
|
|
586
|
+
return additions || deletions ? `<span class="file-delta">${additions}${deletions}</span>` : "";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): string {
|
|
590
|
+
const labels = previewLabels(language);
|
|
591
|
+
const completedAt = state.completedAt ?? (isTerminal(state.status) && state.durationMs !== undefined
|
|
592
|
+
? state.startedAt + state.durationMs
|
|
593
|
+
: undefined);
|
|
594
|
+
const elapsed = `<span>${escapeHtml(labels.elapsed)}<strong${isTerminal(state.status) ? "" : ` data-live-elapsed data-started-at="${state.startedAt}"`}>${completedAt === undefined && isTerminal(state.status) ? escapeHtml(labels.unknown) : formatDuration((completedAt ?? Date.now()) - state.startedAt)}</strong></span>`;
|
|
595
|
+
return [
|
|
596
|
+
elapsed,
|
|
597
|
+
state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
|
|
598
|
+
state.totalTokens === undefined ? "" : `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`,
|
|
599
|
+
state.contextCompactionAfterTokens === undefined ? "" : state.contextCompactionBeforeTokens === undefined
|
|
600
|
+
? `<span title="${escapeAttribute(labels.compactionAfterTokens)}">${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`
|
|
601
|
+
: `<span title="${escapeAttribute(labels.compactionTokens)}">${formatTokenCount(state.contextCompactionBeforeTokens)} → ${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`,
|
|
602
|
+
(state.totalToolCount ?? 0) > 0 ? `<span>${formatNumber(state.totalToolCount ?? 0)} ${escapeHtml(labels.tools)}</span>` : "",
|
|
603
|
+
].filter(Boolean).join("");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function previewTitle(state: TurnViewState): string {
|
|
607
|
+
const value = (state.prompt ?? state.taskTitle ?? "Turn Preview").replace(/\s+/gu, " ").trim();
|
|
608
|
+
return value.length > 80 ? `${value.slice(0, 79)}…` : value;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function renderMarkdown(value: string): string {
|
|
612
|
+
return MARKDOWN.render(value);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function statusLabel(status: TurnViewStatus, language: TurnPreviewLanguage): string {
|
|
616
|
+
return previewLabels(language).statuses[status];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function toolStatusIcon(status: ToolState["status"], language: TurnPreviewLanguage): string {
|
|
620
|
+
return status === "completed"
|
|
621
|
+
? previewLabels(language).toolStatuses.completed
|
|
622
|
+
: status === "failed"
|
|
623
|
+
? previewLabels(language).toolStatuses.failed
|
|
624
|
+
: previewLabels(language).toolStatuses.running;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function toolDuration(tool: ToolState): string | undefined {
|
|
628
|
+
if (tool.startedAt === undefined) return undefined;
|
|
629
|
+
const end = tool.completedAt ?? (tool.status === "running" ? Date.now() : undefined);
|
|
630
|
+
return end === undefined ? undefined : formatDuration(end - tool.startedAt);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function formatDuration(durationMs: number): string {
|
|
634
|
+
const seconds = Math.max(0, Math.floor(durationMs / 1_000));
|
|
635
|
+
const hours = Math.floor(seconds / 3_600);
|
|
636
|
+
const minutes = Math.floor((seconds % 3_600) / 60);
|
|
637
|
+
const remainder = seconds % 60;
|
|
638
|
+
return hours > 0
|
|
639
|
+
? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`
|
|
640
|
+
: `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function formatTimestamp(timestampMs: number): string {
|
|
644
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
645
|
+
hour: "2-digit",
|
|
646
|
+
minute: "2-digit",
|
|
647
|
+
second: "2-digit",
|
|
648
|
+
hour12: false,
|
|
649
|
+
}).format(new Date(timestampMs));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function formatNumber(value: number): string {
|
|
653
|
+
return Math.max(0, Math.floor(value)).toLocaleString("en-US");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatTokenCount(value: number): string {
|
|
657
|
+
return Math.max(0, Math.floor(value)).toLocaleString("en-US", {
|
|
658
|
+
notation: "compact",
|
|
659
|
+
maximumFractionDigits: 1,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function isTerminal(status: TurnViewStatus): boolean {
|
|
664
|
+
return status === "completed" || status === "cancelled" || status === "failed";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function escapeHtml(value: string): string {
|
|
668
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function escapeAttribute(value: string): string {
|
|
672
|
+
return escapeHtml(value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const TURN_PREVIEW_CSS = `
|
|
676
|
+
:root { color-scheme: light dark; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; --page: #fff; --surface: #f5f6f7; --code: #fafafa; --text: #25272b; --muted: #62666d; --line: #e0e2e5; --accent: #245bc0; --success: #257449; --danger: #b52c34; }
|
|
677
|
+
* { box-sizing: border-box; letter-spacing: 0; }
|
|
678
|
+
body { margin: 0; background: var(--page); color: var(--text); font-size: 13px; line-height: 1.55; }
|
|
679
|
+
.page-header { position: sticky; top: 0; z-index: 3; background: var(--page); border-bottom: 1px solid var(--line); }
|
|
680
|
+
.header-inner { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 12px; max-width: 1320px; margin: auto; padding: 6px 20px; }
|
|
681
|
+
.header-top { display: flex; flex-wrap: wrap; align-items: center; min-width: 0; max-width: 100%; gap: 2px 8px; }
|
|
682
|
+
h1 { flex: none; margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; }
|
|
683
|
+
h2 { margin: 0 0 8px; font-size: 13px; font-weight: 650; }
|
|
684
|
+
h2 span { margin-left: 6px; color: var(--muted); font-size: 12px; font-weight: 400; }
|
|
685
|
+
.status, .live { flex: none; font-size: 12px; white-space: nowrap; }
|
|
686
|
+
.status { display: inline-flex; align-items: center; gap: 8px; }
|
|
687
|
+
.status::before { content: "·"; color: var(--muted); }
|
|
688
|
+
.status.starting, .status.running, .status.tool_running, .tool-state.running { color: var(--accent); }
|
|
689
|
+
.status.waiting_for_approval { color: #926a08; }
|
|
690
|
+
.status.completed, .tool-state.completed { color: var(--success); }
|
|
691
|
+
.status.cancelled { color: var(--muted); }
|
|
692
|
+
.status.failed, .tool-state.failed { color: var(--danger); }
|
|
693
|
+
.live { display: flex; align-items: center; gap: 5px; margin-left: auto; color: var(--muted); }
|
|
694
|
+
.live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
695
|
+
.live.connected::before { background: var(--success); }
|
|
696
|
+
.live.disconnected { color: var(--danger); }
|
|
697
|
+
.live.terminal { display: none; }
|
|
698
|
+
.metadata { display: flex; flex: 1 1 360px; flex-wrap: wrap; position: relative; min-width: 0; gap: 2px 8px; padding-left: 12px; color: var(--muted); font-size: 12px; line-height: 1.4; }
|
|
699
|
+
.metadata::before { content: "·"; position: absolute; left: 0; top: 0; }
|
|
700
|
+
.metadata > span { position: relative; min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
|
701
|
+
.metadata > span + span { padding-left: 12px; }
|
|
702
|
+
.metadata > span + span::before { content: "·"; position: absolute; left: 0; top: 0; }
|
|
703
|
+
.metadata strong { color: var(--text); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
704
|
+
main { max-width: 1320px; margin: 0 auto; padding: 0 20px 40px; }
|
|
705
|
+
.plan, .files-summary, .result { padding: 14px 0; border-bottom: 1px solid var(--line); }
|
|
706
|
+
.plan ol { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
707
|
+
.plan li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; overflow-wrap: anywhere; }
|
|
708
|
+
.plan li.completed { color: var(--muted); }
|
|
709
|
+
.plan li.in_progress { color: var(--accent); }
|
|
710
|
+
.timeline { padding-top: 2px; }
|
|
711
|
+
.activity { min-width: 0; }
|
|
712
|
+
.message { padding: 12px 0; border-bottom: 1px solid var(--line); }
|
|
713
|
+
.markdown { min-width: 0; overflow-wrap: anywhere; }
|
|
714
|
+
.commentary > .markdown { font-weight: 700; }
|
|
715
|
+
.markdown > :first-child { margin-top: 0; }
|
|
716
|
+
.markdown > :last-child { margin-bottom: 0; }
|
|
717
|
+
.markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 0 0 6px; }
|
|
718
|
+
.markdown ul, .markdown ol { padding-left: 1.5em; }
|
|
719
|
+
.markdown h1, .markdown h2, .markdown h3 { margin: 12px 0 6px; font-size: 14px; }
|
|
720
|
+
.markdown blockquote { padding-left: 10px; border-left: 2px solid var(--line); color: var(--muted); }
|
|
721
|
+
.markdown a { color: var(--accent); text-underline-offset: 2px; }
|
|
722
|
+
.markdown table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; }
|
|
723
|
+
.markdown th, .markdown td { padding: 5px 8px; border: 1px solid var(--line); }
|
|
724
|
+
.markdown img { max-width: 100%; height: auto; }
|
|
725
|
+
code { padding: 1px 3px; border-radius: 3px; background: var(--surface); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; overflow-wrap: anywhere; }
|
|
726
|
+
pre { max-width: 100%; max-height: 480px; margin: 0; padding: 9px 12px; overflow: auto; background: var(--code); color: var(--text); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; tab-size: 2; white-space: pre; }
|
|
727
|
+
.markdown pre code { padding: 0; background: transparent; color: inherit; }
|
|
728
|
+
details { min-width: 0; }
|
|
729
|
+
summary { display: flex; align-items: center; gap: 8px; min-height: 32px; cursor: pointer; list-style: none; }
|
|
730
|
+
summary::-webkit-details-marker { display: none; }
|
|
731
|
+
summary::before { content: ""; flex: none; width: 6px; height: 6px; margin: 0 3px 0 1px; border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted); transform: rotate(-45deg); }
|
|
732
|
+
details[open] > summary::before { transform: rotate(45deg); }
|
|
733
|
+
summary:hover { background: transparent; }
|
|
734
|
+
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
735
|
+
.tool { margin: 8px 0; }
|
|
736
|
+
.tool-step { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: transparent; }
|
|
737
|
+
.tool-header { display: flex; align-items: flex-start; gap: 8px; min-height: 32px; padding: 7px 10px; overflow-x: auto; background: transparent; font-weight: 400; }
|
|
738
|
+
.tool-state { display: inline-flex; flex: none; align-items: center; justify-content: center; color: var(--muted); font-size: 11px; font-weight: 650; white-space: nowrap; }
|
|
739
|
+
.tool-state.completed { color: var(--success); }
|
|
740
|
+
.tool-state.running { color: var(--accent); }
|
|
741
|
+
.tool-state.failed { color: var(--danger); }
|
|
742
|
+
.tool-title { min-width: 0; flex: 1 0 auto; overflow-wrap: normal; font-size: 12px; font-weight: 400; line-height: 1.45; white-space: pre; }
|
|
743
|
+
.tool-command-title { display: block; min-width: 0; flex: 1 0 auto; margin: 0; padding: 0; overflow-wrap: normal; background: transparent; color: var(--text); font: 12px/1.55 "Cascadia Mono", "SFMono-Regular", Consolas, monospace; font-weight: 400; white-space: pre; }
|
|
744
|
+
.tool-meta { display: flex; flex: 0 1 auto; flex-wrap: nowrap; gap: 12px; max-width: 48%; overflow: hidden; color: var(--muted); font-size: 11px; font-weight: 400; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
745
|
+
.tool-meta span { white-space: nowrap; }
|
|
746
|
+
.tool-body { border-top: 1px solid var(--line); }
|
|
747
|
+
.tool-body > .muted { padding: 8px 12px; }
|
|
748
|
+
.tool-output { border-top: 1px solid var(--line); }
|
|
749
|
+
.tool-output:first-child { border-top: 0; }
|
|
750
|
+
.tool-output > summary { padding: 4px 12px; background: transparent; color: var(--muted); font-size: 12px; }
|
|
751
|
+
.tool-output-label { min-width: 0; }
|
|
752
|
+
.tool-output-meta { display: inline-flex; align-items: center; gap: 12px; margin-left: auto; font-size: 11px; }
|
|
753
|
+
.tool-status-row { display: flex; justify-content: flex-end; padding: 5px 12px; border-top: 1px solid var(--line); }
|
|
754
|
+
.tool-output > pre { max-height: none; border-top: 1px solid var(--line); background: transparent; }
|
|
755
|
+
.repl-code { background: transparent; }
|
|
756
|
+
.repl-result > pre { white-space: pre; overflow-wrap: normal; background: transparent; }
|
|
757
|
+
.error-output > summary, .error-result h2 { color: var(--danger); }
|
|
758
|
+
.muted, .empty { color: var(--muted); }
|
|
759
|
+
.notice, .empty { padding: 10px 0; border-bottom: 1px solid var(--line); }
|
|
760
|
+
.notice { color: #926a08; }
|
|
761
|
+
.files { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
762
|
+
.tool-output .files { padding: 8px 12px; }
|
|
763
|
+
.files li { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
764
|
+
.files code { min-width: 0; }
|
|
765
|
+
.files .file-link { min-width: 0; color: var(--accent); text-underline-offset: 2px; }
|
|
766
|
+
.files .file-link code { color: inherit; }
|
|
767
|
+
.additions { color: var(--success); }
|
|
768
|
+
.deletions { color: var(--danger); }
|
|
769
|
+
.file-delta { display: inline-flex; flex: none; gap: 6px; font: 12px "Cascadia Mono", Consolas, monospace; }
|
|
770
|
+
.tool-image { padding: 8px 12px; border-top: 1px solid var(--line); }
|
|
771
|
+
.tool-image img { display: block; max-width: 100%; max-height: 720px; }
|
|
772
|
+
.final-result h2 { color: var(--success); }
|
|
773
|
+
@media (max-width: 700px) {
|
|
774
|
+
.header-inner { padding: 5px 12px; }
|
|
775
|
+
.header-top { flex-basis: 100%; }
|
|
776
|
+
.metadata { flex-basis: 100%; padding-left: 0; font-size: 11px; }
|
|
777
|
+
.metadata::before { display: none; }
|
|
778
|
+
.live { font-size: 11px; }
|
|
779
|
+
main { padding: 0 12px 28px; }
|
|
780
|
+
.message { padding: 10px 0; }
|
|
781
|
+
.tool-header { gap: 6px; padding: 7px 9px; }
|
|
782
|
+
.tool-state { font-size: 11px; }
|
|
783
|
+
.tool-meta { flex: 0 1 auto; gap: 10px; max-width: 50%; font-size: 11px; }
|
|
784
|
+
.tool-output > summary { min-height: 40px; padding: 6px 10px; }
|
|
785
|
+
.tool-output-meta { gap: 10px; }
|
|
786
|
+
pre { padding: 8px 10px; }
|
|
787
|
+
}
|
|
788
|
+
@media (prefers-color-scheme: dark) {
|
|
789
|
+
:root { --page: #18191b; --surface: #232427; --code: #1d1e20; --text: #e0e1e4; --muted: #a0a3ab; --line: #35373c; --accent: #8aaff7; --success: #83c69c; --danger: #f58c94; }
|
|
790
|
+
.notice, .status.waiting_for_approval { color: #e1bc6f; }
|
|
791
|
+
}
|
|
792
|
+
`;
|