@co0ontty/wand 1.36.0 → 1.37.0
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/dist/claude-sdk-runner.d.ts +7 -0
- package/dist/claude-sdk-runner.js +3 -0
- package/dist/git-quick-commit.js +5 -5
- package/dist/language-prompt.d.ts +17 -0
- package/dist/language-prompt.js +68 -0
- package/dist/process-manager.js +8 -5
- package/dist/prompt-optimizer.js +3 -3
- package/dist/pwa.js +36 -5
- package/dist/server.js +3 -0
- package/dist/structured-session-manager.js +4 -3
- package/dist/web-ui/content/scripts.js +236 -65
- package/dist/web-ui/content/styles.css +258 -71
- package/dist/web-ui/styles.js +18 -4
- package/package.json +1 -1
|
@@ -6,6 +6,13 @@ export declare class ClaudeRunError extends Error {
|
|
|
6
6
|
export interface RunClaudePrintOptions {
|
|
7
7
|
cwd?: string;
|
|
8
8
|
timeoutMs: number;
|
|
9
|
+
/**
|
|
10
|
+
* 用户偏好的回复语言(取自 config.language)。传进来时会以
|
|
11
|
+
* `appendSystemPrompt` 形式灌给 Claude,保证 quick-commit / prompt-optimizer
|
|
12
|
+
* 这种一次性调用也跟用户主会话同语言——之前 wand 的 git commit message 会
|
|
13
|
+
* 莫名其妙变中英混搭,根因就在这。
|
|
14
|
+
*/
|
|
15
|
+
language?: string;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* 用 `@anthropic-ai/claude-agent-sdk` 跑一次"prompt → 单段纯文本"调用,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { query as sdkQuery, } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { buildLanguageDirective } from "./language-prompt.js";
|
|
4
5
|
export class ClaudeRunError extends Error {
|
|
5
6
|
code;
|
|
6
7
|
constructor(message, code) {
|
|
@@ -79,12 +80,14 @@ export async function runClaudePrint(prompt, options) {
|
|
|
79
80
|
const abortController = new AbortController();
|
|
80
81
|
const timeoutHandle = setTimeout(() => abortController.abort(), options.timeoutMs);
|
|
81
82
|
const sdkClaudeBinary = resolveSdkClaudeBinary();
|
|
83
|
+
const languageDirective = options.language ? buildLanguageDirective(options.language) : "";
|
|
82
84
|
const sdkOptions = {
|
|
83
85
|
abortController,
|
|
84
86
|
tools: [],
|
|
85
87
|
persistSession: false,
|
|
86
88
|
...(cwd ? { cwd } : {}),
|
|
87
89
|
...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
|
|
90
|
+
...(languageDirective ? { appendSystemPrompt: languageDirective } : {}),
|
|
88
91
|
};
|
|
89
92
|
// 单条 user message → AsyncGenerator,SDK 的 streaming input 协议要求。
|
|
90
93
|
async function* singleShot() {
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -240,9 +240,9 @@ export class QuickCommitError extends Error {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
// ── AI commit message generation ──
|
|
243
|
-
async function callClaudeText(prompt, cwd) {
|
|
243
|
+
async function callClaudeText(prompt, cwd, language) {
|
|
244
244
|
try {
|
|
245
|
-
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS });
|
|
245
|
+
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS, language });
|
|
246
246
|
}
|
|
247
247
|
catch (error) {
|
|
248
248
|
if (error instanceof ClaudeRunError) {
|
|
@@ -283,7 +283,7 @@ async function generateCommitMessage(cwd, language) {
|
|
|
283
283
|
const diff = collectStagedDiff(cwd);
|
|
284
284
|
const lang = language.trim() || "中文";
|
|
285
285
|
const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
|
|
286
|
-
const raw = await callClaudeText(prompt, cwd);
|
|
286
|
+
const raw = await callClaudeText(prompt, cwd, language);
|
|
287
287
|
const message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
288
288
|
if (!message) {
|
|
289
289
|
throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
@@ -339,7 +339,7 @@ async function generateCommitMessageWithTag(cwd, language) {
|
|
|
339
339
|
|
|
340
340
|
git diff:
|
|
341
341
|
${diff}`;
|
|
342
|
-
const raw = await callClaudeText(prompt, cwd);
|
|
342
|
+
const raw = await callClaudeText(prompt, cwd, language);
|
|
343
343
|
const parsed = tryParseJson(raw);
|
|
344
344
|
let message;
|
|
345
345
|
let suggestedTag;
|
|
@@ -413,7 +413,7 @@ commit message:${commitMessage}
|
|
|
413
413
|
|
|
414
414
|
git diff:
|
|
415
415
|
${diff}`;
|
|
416
|
-
const raw = await callClaudeText(prompt, cwd);
|
|
416
|
+
const raw = await callClaudeText(prompt, cwd, language);
|
|
417
417
|
const parsed = tryParseJson(raw);
|
|
418
418
|
let suggested;
|
|
419
419
|
if (parsed && typeof parsed.tag === "string") {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 强语言指令生成器:供 PTY runner、structured-session-manager(CLI + SDK 两个分支)、
|
|
3
|
+
* 一次性 SDK 调用(claude-sdk-runner.ts)共用。
|
|
4
|
+
*
|
|
5
|
+
* 原本各 runner 散落写 "请使用中文回复" 这种软指令,Claude 写技术内容时还是会条件
|
|
6
|
+
* 反射切英文("Now let me ..."、"OK, ..."),用户设置中文也照样夹英文。
|
|
7
|
+
*
|
|
8
|
+
* 这一版用三招把约束做硬:
|
|
9
|
+
* 1. 明确禁止常见英文起句模式("Now let me"、"Let me check"、"OK,"、"First,"
|
|
10
|
+
* …)并给出反例——给 Claude 一组具体可识别可避免的字串
|
|
11
|
+
* 2. 区分"自然语言"(必须目标语言)和"技术标识符"(路径/命令/API 名可保留原文),
|
|
12
|
+
* 避免 Claude 误以为"代码也要翻译"
|
|
13
|
+
* 3. 自检指令:让 Claude 发现自己即将出错时主动重写
|
|
14
|
+
*
|
|
15
|
+
* 英文模式("English")下 Claude 默认就用英文,只补一句 subagent 透传。
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildLanguageDirective(language: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 强语言指令生成器:供 PTY runner、structured-session-manager(CLI + SDK 两个分支)、
|
|
3
|
+
* 一次性 SDK 调用(claude-sdk-runner.ts)共用。
|
|
4
|
+
*
|
|
5
|
+
* 原本各 runner 散落写 "请使用中文回复" 这种软指令,Claude 写技术内容时还是会条件
|
|
6
|
+
* 反射切英文("Now let me ..."、"OK, ..."),用户设置中文也照样夹英文。
|
|
7
|
+
*
|
|
8
|
+
* 这一版用三招把约束做硬:
|
|
9
|
+
* 1. 明确禁止常见英文起句模式("Now let me"、"Let me check"、"OK,"、"First,"
|
|
10
|
+
* …)并给出反例——给 Claude 一组具体可识别可避免的字串
|
|
11
|
+
* 2. 区分"自然语言"(必须目标语言)和"技术标识符"(路径/命令/API 名可保留原文),
|
|
12
|
+
* 避免 Claude 误以为"代码也要翻译"
|
|
13
|
+
* 3. 自检指令:让 Claude 发现自己即将出错时主动重写
|
|
14
|
+
*
|
|
15
|
+
* 英文模式("English")下 Claude 默认就用英文,只补一句 subagent 透传。
|
|
16
|
+
*/
|
|
17
|
+
export function buildLanguageDirective(language) {
|
|
18
|
+
const trimmed = language?.trim();
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
return "";
|
|
21
|
+
const isChinese = trimmed === "中文";
|
|
22
|
+
const isEnglish = trimmed === "English" || trimmed.toLowerCase() === "english";
|
|
23
|
+
if (isEnglish) {
|
|
24
|
+
return "When you dispatch a subagent via the Task tool, instruct the subagent in its prompt to also respond in English.";
|
|
25
|
+
}
|
|
26
|
+
if (isChinese) {
|
|
27
|
+
return [
|
|
28
|
+
"【语言要求 — 最高优先级】",
|
|
29
|
+
"你必须始终使用中文进行所有自然语言交流。这是硬性约束,不是建议。",
|
|
30
|
+
"",
|
|
31
|
+
"覆盖范围:",
|
|
32
|
+
"- 所有解释、说明、推理、对话、注释、错误描述、TODO 标题、git commit message、思考内容",
|
|
33
|
+
"- 包括开场白、过渡句、状态汇报、回答用户问题",
|
|
34
|
+
"",
|
|
35
|
+
"严禁以下英文起句模式(即使是技术内容也不要用):",
|
|
36
|
+
"- 不要写 \"Now let me ...\"、\"Now I'll ...\"、\"Now remove ...\"——改用 \"现在 ...\" 或直接进入正题",
|
|
37
|
+
"- 不要写 \"Let me check ...\"、\"Let me look at ...\"——改用 \"我看一下 ...\"、\"我检查一下 ...\"",
|
|
38
|
+
"- 不要写 \"OK, ...\"、\"Alright, ...\"、\"Great, ...\"——改用 \"好的\"、\"OK\" 中文或直接省略",
|
|
39
|
+
"- 不要写 \"First, ...\"、\"Then, ...\"、\"Finally, ...\"——改用 \"先\"、\"然后\"、\"最后\"",
|
|
40
|
+
"- 不要写 \"Found it!\"、\"Got it!\"——改用 \"找到了\"、\"明白\"",
|
|
41
|
+
"",
|
|
42
|
+
"可以保留原文的部分(这些不算\"自然语言\"):",
|
|
43
|
+
"- 代码片段、shell 命令、文件路径、URL、API 名、库名、变量名、CSS 属性名等技术标识符",
|
|
44
|
+
"- 引用用户原话、错误信息原文、日志原文",
|
|
45
|
+
"",
|
|
46
|
+
"自检:如果你发现自己即将用英文开始一句话或一段话,立即停下,用中文重新组织语言。",
|
|
47
|
+
"",
|
|
48
|
+
"子代理:当你通过 Task 工具派发 subagent 时,必须在传给 subagent 的 prompt 里明确加上一句中文要求(例如 \"请用中文回复所有自然语言内容\"),保证子代理输出同样遵循。",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
// 其他语言(日语、法语等)——用英文模板,把 language 替进去
|
|
52
|
+
return [
|
|
53
|
+
`[Language requirement — top priority]`,
|
|
54
|
+
`You MUST always use ${trimmed} for all natural-language communication. This is a hard constraint, not a suggestion.`,
|
|
55
|
+
"",
|
|
56
|
+
`Scope: all explanations, narration, reasoning, conversation, comments, error descriptions, TODO titles, git commit messages, and thinking content — including opening phrases, transitions, status updates, and answers to the user.`,
|
|
57
|
+
"",
|
|
58
|
+
`Strictly avoid starting sentences in English (e.g. "Now let me ...", "Let me check ...", "OK, ...", "First, ...", "Found it!"). Use the equivalent ${trimmed} phrasing instead, or skip the transition.`,
|
|
59
|
+
"",
|
|
60
|
+
`What may stay in its original form (these are NOT natural language):`,
|
|
61
|
+
`- Code, shell commands, file paths, URLs, API/library/variable names, CSS properties, other technical identifiers`,
|
|
62
|
+
`- Direct quotes of user input, raw error messages, raw log lines`,
|
|
63
|
+
"",
|
|
64
|
+
`Self-check: if you notice you are about to start a sentence in English, stop and rewrite it in ${trimmed}.`,
|
|
65
|
+
"",
|
|
66
|
+
`Subagent: when you dispatch a subagent via the Task tool, you MUST explicitly instruct the subagent in its prompt to also respond in ${trimmed}.`,
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
package/dist/process-manager.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
|
10
10
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
11
11
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
12
12
|
import { buildChildEnv } from "./env-utils.js";
|
|
13
|
+
import { buildLanguageDirective } from "./language-prompt.js";
|
|
13
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
14
15
|
import { getResumeCommandSessionId } from "./resume-policy.js";
|
|
15
16
|
import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
|
|
@@ -1657,11 +1658,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
1657
1658
|
result += ` --append-system-prompt '${escaped}'`;
|
|
1658
1659
|
}
|
|
1659
1660
|
if (language) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1661
|
+
// 与 structured-session-manager.ts 走同一个 buildLanguageDirective,保证 PTY 与
|
|
1662
|
+
// structured 两种 runner 用同一条强约束指令——避免"换个模式 Claude 又开始夹英文"。
|
|
1663
|
+
const langPrompt = buildLanguageDirective(language);
|
|
1664
|
+
if (langPrompt) {
|
|
1665
|
+
const escaped = langPrompt.replace(/'/g, "'\\''");
|
|
1666
|
+
result += ` --append-system-prompt '${escaped}'`;
|
|
1667
|
+
}
|
|
1665
1668
|
}
|
|
1666
1669
|
return result;
|
|
1667
1670
|
}
|
package/dist/prompt-optimizer.js
CHANGED
|
@@ -9,9 +9,9 @@ export class PromptOptimizeError extends Error {
|
|
|
9
9
|
this.name = "PromptOptimizeError";
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
async function callClaudeText(prompt, cwd) {
|
|
12
|
+
async function callClaudeText(prompt, cwd, language) {
|
|
13
13
|
try {
|
|
14
|
-
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_TIMEOUT_MS });
|
|
14
|
+
return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_TIMEOUT_MS, language });
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
17
17
|
if (error instanceof ClaudeRunError) {
|
|
@@ -50,7 +50,7 @@ export async function optimizePrompt(rawText, language, cwd) {
|
|
|
50
50
|
throw new PromptOptimizeError(`输入过长(${text.length} 字符),请缩短到 ${MAX_INPUT_LENGTH} 以内。`, "INPUT_TOO_LONG");
|
|
51
51
|
}
|
|
52
52
|
const prompt = buildOptimizePrompt(text, language);
|
|
53
|
-
const raw = await callClaudeText(prompt, cwd);
|
|
53
|
+
const raw = await callClaudeText(prompt, cwd, language);
|
|
54
54
|
const cleaned = raw
|
|
55
55
|
.replace(/^```[a-zA-Z]*\n?/, "")
|
|
56
56
|
.replace(/\n?```$/, "")
|
package/dist/pwa.js
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PWA manifest and Service Worker generation.
|
|
3
3
|
*/
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
4
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
10
10
|
const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0";
|
|
11
|
-
/** Cache version
|
|
12
|
-
|
|
11
|
+
/** Cache version: package version + content fingerprint.
|
|
12
|
+
*
|
|
13
|
+
* 之前只用 pkgVersion 派生,本地 dev 时同一个 1.36.0 下改了几次 CSS / scripts,
|
|
14
|
+
* SW 的 cache key 都没变 → 旧的 RUNTIME_CACHE 会一直回放老 HTML 给已安装 PWA,
|
|
15
|
+
* 用户表现是"我明明改了 UI,怎么手机上一点变化都没有"。
|
|
16
|
+
* 把内容 mtime 也喂进 hash,dev iterate 时每次磁盘改动都换 cache key,
|
|
17
|
+
* SW activate 会把不匹配前缀的缓存 keys 删掉,从而强制重拉。
|
|
18
|
+
* 正式发版时由于 pkgVersion 也会变,效果叠加,无副作用。
|
|
19
|
+
*/
|
|
20
|
+
function buildCacheVersion() {
|
|
21
|
+
const h = createHash("md5").update(pkgVersion);
|
|
22
|
+
const fingerprintTargets = [
|
|
23
|
+
path.join(__dirname, "web-ui", "content", "scripts.js"),
|
|
24
|
+
path.join(__dirname, "web-ui", "content", "styles.css"),
|
|
25
|
+
];
|
|
26
|
+
for (const p of fingerprintTargets) {
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(p)) {
|
|
29
|
+
const s = statSync(p);
|
|
30
|
+
h.update(":").update(p).update(":").update(String(s.mtimeMs)).update(":").update(String(s.size));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// best effort — fingerprint 只是为了 bust 缓存,失败就退化成 pkg-only
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return h.digest("hex").slice(0, 8);
|
|
38
|
+
}
|
|
39
|
+
// 不 freeze 进模块加载时——SW JS 是每次请求 generateServiceWorker() 现拼的,
|
|
40
|
+
// 这里也对应每次现算,dev 改 CSS 不用重启进程都能 bust 浏览器/PWA 端缓存。
|
|
41
|
+
// 缓存层会自己做 mtime check(styles.ts 的 mtime 缓存模式同思路),这里直接
|
|
42
|
+
// statSync,磁盘命中本地 fs 几十微秒级,可忽略。
|
|
13
43
|
export function generatePwaManifest() {
|
|
14
44
|
return JSON.stringify({
|
|
15
45
|
id: "/wand",
|
|
@@ -44,9 +74,10 @@ export function generatePwaManifest() {
|
|
|
44
74
|
});
|
|
45
75
|
}
|
|
46
76
|
export function generateServiceWorker() {
|
|
77
|
+
const cacheVersion = buildCacheVersion();
|
|
47
78
|
return `
|
|
48
|
-
const STATIC_CACHE = 'wand-static-${
|
|
49
|
-
const RUNTIME_CACHE = 'wand-runtime-${
|
|
79
|
+
const STATIC_CACHE = 'wand-static-${cacheVersion}';
|
|
80
|
+
const RUNTIME_CACHE = 'wand-runtime-${cacheVersion}';
|
|
50
81
|
const APP_SHELL = '/';
|
|
51
82
|
const STATIC_ASSETS = [
|
|
52
83
|
'/manifest.json',
|
package/dist/server.js
CHANGED
|
@@ -1097,6 +1097,9 @@ export async function startServer(config, configPath) {
|
|
|
1097
1097
|
],
|
|
1098
1098
|
structuredChatPersona,
|
|
1099
1099
|
cardDefaults: config.cardDefaults,
|
|
1100
|
+
// 把语言偏好暴露给前端做 UI 文案 i18n。后端原本只用它给 Claude 拼 system prompt,
|
|
1101
|
+
// 前端没收到 → "SUBAGENT" / "Read" 这些 UI label 一直是英文,跟用户设的中文不匹配。
|
|
1102
|
+
language: config.language ?? "",
|
|
1100
1103
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
1101
1104
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
1102
1105
|
currentVersion: PKG_VERSION,
|
|
@@ -8,6 +8,7 @@ import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
|
8
8
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
9
9
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
10
10
|
import { buildChildEnv } from "./env-utils.js";
|
|
11
|
+
import { buildLanguageDirective } from "./language-prompt.js";
|
|
11
12
|
function defaultStructuredRunner(provider) {
|
|
12
13
|
return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
|
|
13
14
|
}
|
|
@@ -389,9 +390,9 @@ function buildAppendSystemPromptParts(language, mode) {
|
|
|
389
390
|
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
390
391
|
}
|
|
391
392
|
if (trimmedLanguage) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
393
|
+
const directive = buildLanguageDirective(trimmedLanguage);
|
|
394
|
+
if (directive)
|
|
395
|
+
parts.push(directive);
|
|
395
396
|
}
|
|
396
397
|
return parts;
|
|
397
398
|
}
|
|
@@ -384,6 +384,81 @@
|
|
|
384
384
|
})()
|
|
385
385
|
};
|
|
386
386
|
|
|
387
|
+
// ── 前端 i18n(最小化)──
|
|
388
|
+
// 后端 config.language 是给 Claude 用的"回答语言"偏好("中文" / "English" / 任意字符串),
|
|
389
|
+
// 之前 frontend 完全没收 → UI label 一直 hardcoded 中文 + 个别英文("SUBAGENT" 那个 tag)。
|
|
390
|
+
// 用户设的是中文时,"SUBAGENT" 这类英文残留就和"配置语言不一致"。
|
|
391
|
+
//
|
|
392
|
+
// 设计取舍:
|
|
393
|
+
// - 只维护两套:中文(默认) + 英文。其它取值("日本語"、"Français"等)回退到英文,
|
|
394
|
+
// 因为 Claude 会按用户语言回答,UI 至少不卡在中文上让英语圈用户看不懂。
|
|
395
|
+
// - 不引入 i18n 库,几十个 key 用平铺对象,t(key, params) 是个十行 helper。
|
|
396
|
+
// - params 支持 "{name}" 占位符替换,避免在调用点拼字符串。
|
|
397
|
+
// - 缺 key 时回退到中文表,再没有就返回 key 本身(debug 友好)。
|
|
398
|
+
var I18N_DEFAULT_LANG = "中文";
|
|
399
|
+
var I18N = {
|
|
400
|
+
"中文": {
|
|
401
|
+
"subagent.tag": "子代理",
|
|
402
|
+
"subagent.handoff": "{parent} 让 {sub} 帮忙",
|
|
403
|
+
"subagent.handoff.with_desc": "{parent} 让 {sub} 帮忙:",
|
|
404
|
+
"subagent.continued": "继续输出",
|
|
405
|
+
"subagent.task.done": "任务完成",
|
|
406
|
+
"subagent.task.failed": "任务失败",
|
|
407
|
+
"subagent.running": "运行中",
|
|
408
|
+
"subagent.no_output": "(无输出)",
|
|
409
|
+
"subagent.helper_fallback_prefix": "协作猫·",
|
|
410
|
+
"subagent.title_aria": "点击展开 / 收起子代理输出",
|
|
411
|
+
"subagent.tag_title": "子代理 / subagent",
|
|
412
|
+
"ui.expand": "展开",
|
|
413
|
+
"ui.collapse": "收起",
|
|
414
|
+
"ui.expand_panel_aria": "展开子代理输出",
|
|
415
|
+
"ui.collapse_panel_aria": "收起子代理输出"
|
|
416
|
+
},
|
|
417
|
+
"English": {
|
|
418
|
+
"subagent.tag": "Subagent",
|
|
419
|
+
"subagent.handoff": "{parent} asked {sub} for help",
|
|
420
|
+
"subagent.handoff.with_desc": "{parent} asked {sub} for help with: ",
|
|
421
|
+
"subagent.continued": "continued",
|
|
422
|
+
"subagent.task.done": "Task complete",
|
|
423
|
+
"subagent.task.failed": "Task failed",
|
|
424
|
+
"subagent.running": "Running",
|
|
425
|
+
"subagent.no_output": "(no output)",
|
|
426
|
+
"subagent.helper_fallback_prefix": "Helper·",
|
|
427
|
+
"subagent.title_aria": "Click to expand / collapse subagent output",
|
|
428
|
+
"subagent.tag_title": "Subagent",
|
|
429
|
+
"ui.expand": "Expand",
|
|
430
|
+
"ui.collapse": "Collapse",
|
|
431
|
+
"ui.expand_panel_aria": "Expand subagent output",
|
|
432
|
+
"ui.collapse_panel_aria": "Collapse subagent output"
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
function getActiveLang() {
|
|
436
|
+
var raw = state.config && typeof state.config.language === "string" ? state.config.language.trim() : "";
|
|
437
|
+
if (!raw) return I18N_DEFAULT_LANG;
|
|
438
|
+
if (I18N[raw]) return raw;
|
|
439
|
+
// 模糊匹配:用户可能写 "english" / "en" / "ENG"
|
|
440
|
+
var lower = raw.toLowerCase();
|
|
441
|
+
if (lower === "english" || lower === "en" || lower.indexOf("english") === 0 || lower.indexOf("英") === 0) return "English";
|
|
442
|
+
if (lower === "中文" || lower === "zh" || lower.indexOf("zh") === 0 || lower.indexOf("中") === 0 || lower.indexOf("chinese") === 0) return "中文";
|
|
443
|
+
return "English"; // 其它语言走英文 fallback(Claude 会按 raw 回答,UI 至少英文不卡)
|
|
444
|
+
}
|
|
445
|
+
function t(key, params) {
|
|
446
|
+
var lang = getActiveLang();
|
|
447
|
+
var table = I18N[lang] || I18N[I18N_DEFAULT_LANG];
|
|
448
|
+
var template = table && key in table ? table[key] : null;
|
|
449
|
+
if (template == null) {
|
|
450
|
+
var def = I18N[I18N_DEFAULT_LANG];
|
|
451
|
+
template = def && key in def ? def[key] : key;
|
|
452
|
+
}
|
|
453
|
+
if (params && typeof template === "string") {
|
|
454
|
+
for (var k in params) {
|
|
455
|
+
if (!Object.prototype.hasOwnProperty.call(params, k)) continue;
|
|
456
|
+
template = template.split("{" + k + "}").join(params[k]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return template;
|
|
460
|
+
}
|
|
461
|
+
|
|
387
462
|
// ── 统一线性图标库 ──
|
|
388
463
|
// 替代页面里散落的 emoji(🛡 / ⌨ / 📁 / 🔔 …)。这些 emoji 在系统字体里渲染成
|
|
389
464
|
// 彩色卡通形态,与项目温暖米色 + 棕橙的复古主题视觉冲突明显。这里集中维护
|
|
@@ -1070,6 +1145,10 @@
|
|
|
1070
1145
|
}
|
|
1071
1146
|
case "tool-group":
|
|
1072
1147
|
return el.getAttribute("data-expanded") === "true";
|
|
1148
|
+
case "subagent-reply":
|
|
1149
|
+
return el.getAttribute("data-expanded") === "true";
|
|
1150
|
+
case "subagent-panel":
|
|
1151
|
+
return el.getAttribute("data-expanded") === "true";
|
|
1073
1152
|
default:
|
|
1074
1153
|
return false;
|
|
1075
1154
|
}
|
|
@@ -1118,6 +1197,35 @@
|
|
|
1118
1197
|
if (chevron) chevron.style.transform = expanded ? "rotate(180deg)" : "";
|
|
1119
1198
|
break;
|
|
1120
1199
|
}
|
|
1200
|
+
case "subagent-reply": {
|
|
1201
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
1202
|
+
var subLabel = el.querySelector(".subagent-reply-toggle-label");
|
|
1203
|
+
if (subLabel) subLabel.textContent = expanded ? "收起" : "展开";
|
|
1204
|
+
var subToggleBtn = el.querySelector(".subagent-reply-toggle");
|
|
1205
|
+
if (subToggleBtn) {
|
|
1206
|
+
subToggleBtn.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1207
|
+
subToggleBtn.setAttribute("aria-label", expanded ? "收起子代理回复" : "展开子代理回复全文");
|
|
1208
|
+
}
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
case "subagent-panel": {
|
|
1212
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
1213
|
+
// 头/尾两个按钮都得同步——label、aria-expanded、aria-label
|
|
1214
|
+
var panelBtns = el.querySelectorAll(".subagent-panel-toggle");
|
|
1215
|
+
for (var pbi = 0; pbi < panelBtns.length; pbi++) {
|
|
1216
|
+
var pb = panelBtns[pbi];
|
|
1217
|
+
pb.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1218
|
+
pb.setAttribute("aria-label", expanded ? "收起子代理输出" : "展开子代理输出");
|
|
1219
|
+
var pblbl = pb.querySelector(".subagent-panel-toggle-label");
|
|
1220
|
+
if (pblbl) pblbl.textContent = expanded ? "收起" : "展开";
|
|
1221
|
+
}
|
|
1222
|
+
// 展开时把 body 滚到顶,避免延续上次的滚动位置造成"展开后看到一半"
|
|
1223
|
+
if (expanded) {
|
|
1224
|
+
var pbody = el.querySelector(".subagent-panel-body");
|
|
1225
|
+
if (pbody) pbody.scrollTop = 0;
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1121
1229
|
}
|
|
1122
1230
|
}
|
|
1123
1231
|
|
|
@@ -5658,35 +5766,28 @@
|
|
|
5658
5766
|
}
|
|
5659
5767
|
persistElementExpandState(el, "thinking");
|
|
5660
5768
|
};
|
|
5661
|
-
// Toggle function for subagent reply bubbles —
|
|
5662
|
-
//
|
|
5663
|
-
|
|
5769
|
+
// Toggle function for subagent reply bubbles — simple two-state preview/expanded.
|
|
5770
|
+
// 参考 opencode 的折叠面板:默认固定高度预览(含底部渐隐 mask),点击切到全文展开。
|
|
5771
|
+
// 状态写在 data-expanded 上,配套 CSS 控制 max-height + mask;用 data-expand-key
|
|
5772
|
+
// 走通用持久化通道(applyPersistedExpandState 会自动恢复用户上次的选择)。
|
|
5773
|
+
window.__subagentReplyToggle = function(e, target) {
|
|
5664
5774
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5665
|
-
var bubble =
|
|
5775
|
+
var bubble = target && target.closest ? target.closest(".subagent-reply") : null;
|
|
5666
5776
|
if (!bubble) return;
|
|
5667
|
-
var
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
if (
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
icon.textContent = next === "collapsed" ? "▸"
|
|
5682
|
-
: next === "expanded" ? "▴"
|
|
5683
|
-
: "▾";
|
|
5684
|
-
}
|
|
5685
|
-
btn.setAttribute("aria-label",
|
|
5686
|
-
next === "preview" ? "点击展开全部" :
|
|
5687
|
-
next === "expanded" ? "点击完全收起" :
|
|
5688
|
-
"点击切回预览"
|
|
5689
|
-
);
|
|
5777
|
+
var expanded = bubble.getAttribute("data-expanded") === "true";
|
|
5778
|
+
applyExpandedState(bubble, "subagent-reply", !expanded);
|
|
5779
|
+
persistElementExpandState(bubble, "subagent-reply");
|
|
5780
|
+
};
|
|
5781
|
+
// Toggle function for whole subagent panel (handoff + body + footer)。
|
|
5782
|
+
// 头部 / 尾部按钮、整条 header 都绑这个;data-expanded 一变,CSS 切 body max-height
|
|
5783
|
+
// + 旋转 chevron,applyExpandedState 走通用通道写持久化。
|
|
5784
|
+
window.__subagentPanelToggle = function(e, target) {
|
|
5785
|
+
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5786
|
+
var panel = target && target.closest ? target.closest(".subagent-panel") : null;
|
|
5787
|
+
if (!panel) return;
|
|
5788
|
+
var expanded = panel.getAttribute("data-expanded") === "true";
|
|
5789
|
+
applyExpandedState(panel, "subagent-panel", !expanded);
|
|
5790
|
+
persistElementExpandState(panel, "subagent-panel");
|
|
5690
5791
|
};
|
|
5691
5792
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
5692
5793
|
window.__inlineToolToggle = function(el) {
|
|
@@ -18905,8 +19006,8 @@
|
|
|
18905
19006
|
var agentType = sub.agentType || "";
|
|
18906
19007
|
if (agentType && SUBAGENT_NAME_MAP[agentType]) return SUBAGENT_NAME_MAP[agentType];
|
|
18907
19008
|
if (agentType) return agentType;
|
|
18908
|
-
var tail = (sub.taskId || "").slice(-4) || "未知";
|
|
18909
|
-
return "
|
|
19009
|
+
var tail = (sub.taskId || "").slice(-4) || (getActiveLang() === "English" ? "?" : "未知");
|
|
19010
|
+
return t("subagent.helper_fallback_prefix") + tail;
|
|
18910
19011
|
}
|
|
18911
19012
|
function getSubagentPalette(sub) {
|
|
18912
19013
|
// 哈希优先用 agentType,让同类型 agent 跨 turn 颜色稳定;没有 agentType 时
|
|
@@ -18924,33 +19025,35 @@
|
|
|
18924
19025
|
'</div>';
|
|
18925
19026
|
}
|
|
18926
19027
|
|
|
18927
|
-
// subagent
|
|
18928
|
-
//
|
|
19028
|
+
// subagent 最终回复(父 Task 的 tool_result)——现在外层 .subagent-panel 已经
|
|
19029
|
+
// 负责整段折叠 / 滚动,这里只需把"任务完成 / 失败"做个轻量标记块,markdown
|
|
19030
|
+
// 内容平铺,让 panel 的 body 滚动条统一接管。
|
|
18929
19031
|
function renderSubagentReplyBubble(block, role) {
|
|
18930
19032
|
if (!block || block.type !== "tool_result") return "";
|
|
18931
19033
|
var text = extractToolResultText(block.content);
|
|
18932
19034
|
var isError = block.is_error === true;
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
|
|
18937
|
-
|
|
19035
|
+
var rawText = typeof text === "string" ? text : (text == null ? "" : String(text));
|
|
19036
|
+
|
|
19037
|
+
// pending:subagent 还在跑,没收到结果。在 panel body 里画一个 typing 指示器
|
|
19038
|
+
// 占位,告诉用户"还在跑"。
|
|
19039
|
+
if (!isError && !rawText.trim()) {
|
|
19040
|
+
return '<div class="subagent-reply pending">' +
|
|
19041
|
+
'<span class="subagent-reply-marker pending">' + escapeHtml(t("subagent.running")) + '</span>' +
|
|
19042
|
+
'<span class="typing-indicator"><span></span><span></span><span></span></span>' +
|
|
18938
19043
|
'</div>';
|
|
18939
19044
|
}
|
|
18940
19045
|
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
19046
|
+
var displayText = rawText.trim() ? rawText : t("subagent.no_output");
|
|
19047
|
+
var bodyHtml = rawText.trim() ? renderMarkdown(displayText) : escapeHtml(displayText);
|
|
19048
|
+
var markerLabel = isError ? t("subagent.task.failed") : t("subagent.task.done");
|
|
19049
|
+
var markerSymbol = isError ? "✗" : "✓";
|
|
18944
19050
|
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
'
|
|
18950
|
-
'<
|
|
18951
|
-
'<span class="subagent-reply-cycle-label">展开</span>' +
|
|
18952
|
-
'<span class="subagent-reply-cycle-icon" aria-hidden="true">▾</span>' +
|
|
18953
|
-
'</button>' +
|
|
19051
|
+
return '<div class="subagent-reply final' + (isError ? ' error' : '') + '">' +
|
|
19052
|
+
'<div class="subagent-reply-marker ' + (isError ? 'error' : 'done') + '">' +
|
|
19053
|
+
'<span class="subagent-reply-marker-icon" aria-hidden="true">' + markerSymbol + '</span>' +
|
|
19054
|
+
'<span class="subagent-reply-marker-label">' + escapeHtml(markerLabel) + '</span>' +
|
|
19055
|
+
'</div>' +
|
|
19056
|
+
'<div class="subagent-reply-content">' + bodyHtml + '</div>' +
|
|
18954
19057
|
'</div>';
|
|
18955
19058
|
}
|
|
18956
19059
|
var PIXEL_AVATAR = {
|
|
@@ -19301,23 +19404,12 @@
|
|
|
19301
19404
|
// 跳过整段。否则会渲染出"只有头像没内容"的空气泡。
|
|
19302
19405
|
if (!segHtml || !segHtml.trim()) continue;
|
|
19303
19406
|
if (seg.subagent) {
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
19311
|
-
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
19312
|
-
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>' +
|
|
19313
|
-
'<span class="chat-handoff-tag" title="子代理 / subagent">subagent</span>' +
|
|
19314
|
-
'帮忙' + desc +
|
|
19315
|
-
'</div>';
|
|
19316
|
-
}
|
|
19317
|
-
html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|
|
19318
|
-
subagentAvatarHtml(seg.subagent) +
|
|
19319
|
-
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
19320
|
-
'</div>';
|
|
19407
|
+
// 整段 subagent 输出包成一个统一的可折叠面板:
|
|
19408
|
+
// 头部 = handoff title + 展开按钮;body = segHtml(所有工具卡、文本、最终回复
|
|
19409
|
+
// 都在 body 里);footer = 同款按钮。同一 taskId 的连续段(极少出现的
|
|
19410
|
+
// parent→sub→parent→sub 交错)只在第一次露 handoff title。
|
|
19411
|
+
var includeHandoff = showHandoff && lastSubId !== seg.subagent.taskId;
|
|
19412
|
+
html += buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff);
|
|
19321
19413
|
lastSubId = seg.subagent.taskId;
|
|
19322
19414
|
} else {
|
|
19323
19415
|
html += '<div class="chat-message-segment parent">' +
|
|
@@ -19330,6 +19422,85 @@
|
|
|
19330
19422
|
return html;
|
|
19331
19423
|
}
|
|
19332
19424
|
|
|
19425
|
+
// 渲染整段 subagent 输出为一个可折叠面板:
|
|
19426
|
+
// ┌─ subagent-panel ──────────────────────────────────┐
|
|
19427
|
+
// │ [🐱] ↳ 勤劳初二 让 协作猫 帮忙:xxx [展开 ▾] │ ← header (always visible)
|
|
19428
|
+
// ├───────────────────────────────────────────────────┤
|
|
19429
|
+
// │ <tool 卡 1> │
|
|
19430
|
+
// │ <text> │ ← body
|
|
19431
|
+
// │ <tool 卡 2> │ 默认 max-height 22em
|
|
19432
|
+
// │ <... 最终回复 ...> │ + 内部 overflow-y:auto
|
|
19433
|
+
// ├───────────────────────────────────────────────────┤
|
|
19434
|
+
// │ [展开 ▾] │ ← footer (always visible)
|
|
19435
|
+
// └───────────────────────────────────────────────────┘
|
|
19436
|
+
// 状态写在 .subagent-panel[data-expanded],按 messageKey + taskId 持久化。
|
|
19437
|
+
// 默认折叠(preview);用户点头/尾按钮或头部标题条都能切。
|
|
19438
|
+
function buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff) {
|
|
19439
|
+
var sub = seg.subagent;
|
|
19440
|
+
var subPalette = getSubagentPalette(sub);
|
|
19441
|
+
var subName = getSubagentDisplayName(sub);
|
|
19442
|
+
var taskId = sub.taskId || "";
|
|
19443
|
+
var avatarSvg = buildPixelSvg(buildCatGrid(subPalette));
|
|
19444
|
+
|
|
19445
|
+
var titleHtml;
|
|
19446
|
+
if (includeHandoff) {
|
|
19447
|
+
// 用 i18n 模板拼 handoff title。{parent}/{sub} 用占位符避免在调用点拼字符串导致
|
|
19448
|
+
// 顺序错乱(中文 "X 让 Y 帮忙",英文 "X asked Y for help",词序完全不同)。
|
|
19449
|
+
// tag 是单独的彩色 chip,所以把模板里的 {sub} 替换成空 span 占位,然后再 splice
|
|
19450
|
+
// 进 <strong> + chip——保证模板可读、调用点不脏。
|
|
19451
|
+
var subInlineHtml = '<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
|
|
19452
|
+
'<span class="subagent-panel-tag" title="' + escapeHtml(t("subagent.tag_title")) + '">' + escapeHtml(t("subagent.tag")) + '</span>';
|
|
19453
|
+
var hasDesc = !!(sub.taskDescription && String(sub.taskDescription).trim());
|
|
19454
|
+
var handoffTpl = hasDesc ? t("subagent.handoff.with_desc", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml })
|
|
19455
|
+
: t("subagent.handoff", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml });
|
|
19456
|
+
var descSpan = hasDesc
|
|
19457
|
+
? '<span class="subagent-panel-task-desc">' + escapeHtml(sub.taskDescription) + '</span>'
|
|
19458
|
+
: '';
|
|
19459
|
+
titleHtml = '<span class="subagent-panel-arrow" aria-hidden="true">↳</span>' +
|
|
19460
|
+
'<span class="subagent-panel-attribution">' + handoffTpl + descSpan + '</span>';
|
|
19461
|
+
} else {
|
|
19462
|
+
titleHtml = '<span class="subagent-panel-attribution">' +
|
|
19463
|
+
'<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
|
|
19464
|
+
'<span class="subagent-panel-task-desc"> ' + escapeHtml(t("subagent.continued")) + '</span>' +
|
|
19465
|
+
'</span>';
|
|
19466
|
+
}
|
|
19467
|
+
|
|
19468
|
+
var expandKey = buildExpandKey("subagent-panel", [messageKey, taskId]);
|
|
19469
|
+
var persisted = getPersistedExpandState(expandKey);
|
|
19470
|
+
var expanded = persisted === null ? false : !!persisted;
|
|
19471
|
+
|
|
19472
|
+
function toggleBtnHtml(position) {
|
|
19473
|
+
return '<button type="button" class="subagent-panel-toggle" ' +
|
|
19474
|
+
'data-position="' + position + '" ' +
|
|
19475
|
+
'onclick="__subagentPanelToggle(event, this)" ' +
|
|
19476
|
+
'aria-expanded="' + (expanded ? "true" : "false") + '" ' +
|
|
19477
|
+
'aria-label="' + escapeHtml(expanded ? t("ui.collapse_panel_aria") : t("ui.expand_panel_aria")) + '">' +
|
|
19478
|
+
'<span class="subagent-panel-toggle-label">' + escapeHtml(expanded ? t("ui.collapse") : t("ui.expand")) + '</span>' +
|
|
19479
|
+
'<svg class="subagent-panel-toggle-icon" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">' +
|
|
19480
|
+
'<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
19481
|
+
'</svg>' +
|
|
19482
|
+
'</button>';
|
|
19483
|
+
}
|
|
19484
|
+
|
|
19485
|
+
return '<div class="subagent-panel" ' +
|
|
19486
|
+
'data-expand-kind="subagent-panel" ' +
|
|
19487
|
+
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
19488
|
+
'data-agent-id="' + escapeHtml(taskId) + '" ' +
|
|
19489
|
+
'data-expanded="' + (expanded ? "true" : "false") + '" ' +
|
|
19490
|
+
'style="--agent-color:' + subPalette.primary + '">' +
|
|
19491
|
+
'<div class="subagent-panel-header" onclick="__subagentPanelToggle(event, this)" role="button" tabindex="0" aria-label="' + escapeHtml(t("subagent.title_aria")) + '">' +
|
|
19492
|
+
'<span class="subagent-panel-avatar" aria-hidden="true">' + avatarSvg + '</span>' +
|
|
19493
|
+
titleHtml +
|
|
19494
|
+
toggleBtnHtml("top") +
|
|
19495
|
+
'</div>' +
|
|
19496
|
+
'<div class="subagent-panel-body">' + segHtml + '</div>' +
|
|
19497
|
+
'<div class="subagent-panel-footer">' +
|
|
19498
|
+
'<span class="subagent-panel-footer-hint" aria-hidden="true">— ' + escapeHtml(subName) + ' —</span>' +
|
|
19499
|
+
toggleBtnHtml("bottom") +
|
|
19500
|
+
'</div>' +
|
|
19501
|
+
'</div>';
|
|
19502
|
+
}
|
|
19503
|
+
|
|
19333
19504
|
function renderStructuredMessage(msg, roundUsage, messageIndex, legacyTaskMap) {
|
|
19334
19505
|
var role = msg.role;
|
|
19335
19506
|
var messageKey = getMessageKey(msg, messageIndex);
|
|
@@ -112,9 +112,21 @@
|
|
|
112
112
|
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/*
|
|
115
|
+
/* 尊重用户的减少动画偏好。
|
|
116
|
+
但 Android WebView 例外: Chromium 在 Android 上会把系统 Developer
|
|
117
|
+
Options 里的 "Animator duration scale" / "Transition animation scale"
|
|
118
|
+
(设为 0 = 关) 直接映射成 prefers-reduced-motion: reduce — 这俩开关其实
|
|
119
|
+
是为 Android 原生 UI 转场设计的, 不少老用户为求"启动快"长期关着,
|
|
120
|
+
省电模式也会强制关掉。结果就是 Wand APK 内所有入场动画 (queueBarEnter、
|
|
121
|
+
messageSlideUser 等) 被压成 0.01ms, 用户看不到"消息已加入排队"这种
|
|
122
|
+
关键反馈, 跟浏览器版表现不一致。
|
|
123
|
+
html.is-wand-app 由 scripts.js 在 UA 含 WandApp/* 时挂到 <html> 上 ——
|
|
124
|
+
Wand APK 内自己定义动画策略, 不跟 Android 系统级开关绑死; 浏览器 /
|
|
125
|
+
普通 PWA / 桌面端仍然完整尊重用户偏好 (排除 .is-wand-app 后规则不变)。 */
|
|
116
126
|
@media (prefers-reduced-motion: reduce) {
|
|
117
|
-
*,
|
|
127
|
+
html:not(.is-wand-app) *,
|
|
128
|
+
html:not(.is-wand-app) *::before,
|
|
129
|
+
html:not(.is-wand-app) *::after {
|
|
118
130
|
animation-duration: 0.01ms !important;
|
|
119
131
|
animation-iteration-count: 1 !important;
|
|
120
132
|
transition-duration: 0.01ms !important;
|
|
@@ -4380,7 +4392,7 @@
|
|
|
4380
4392
|
的 WebView,会把 color-mix(...) 当作 invalid 整条扔掉,导致背景/边框消失。
|
|
4381
4393
|
下面凡是 color-mix 的属性都先写一条 rgba()/纯色 fallback,再被 color-mix
|
|
4382
4394
|
覆盖;旧浏览器看到 fallback、新浏览器自动用 color-mix。同一文件里下方
|
|
4383
|
-
.chat-handoff-tag / .subagent-reply / .subagent-reply-
|
|
4395
|
+
.chat-handoff-tag / .subagent-reply / .subagent-reply-toggle 等也是同思路。 */
|
|
4384
4396
|
.chat-handoff {
|
|
4385
4397
|
align-self: flex-start;
|
|
4386
4398
|
margin: 4px 0 2px 4px;
|
|
@@ -4466,15 +4478,18 @@
|
|
|
4466
4478
|
padding: 14px 16px;
|
|
4467
4479
|
}
|
|
4468
4480
|
|
|
4481
|
+
/* 短内容(error)形态:图标 + body 并列。用 flex 而不是 inline-block + display:inline,
|
|
4482
|
+
后者在 body 内含块级 markdown(段落 / 代码块)时会塌掉。 */
|
|
4483
|
+
.subagent-reply:not(.collapsible) {
|
|
4484
|
+
display: flex;
|
|
4485
|
+
align-items: flex-start;
|
|
4486
|
+
gap: 6px;
|
|
4487
|
+
}
|
|
4469
4488
|
.subagent-reply-icon {
|
|
4470
|
-
|
|
4489
|
+
flex: 0 0 auto;
|
|
4471
4490
|
color: var(--danger, #e2574c);
|
|
4472
|
-
margin-right: 6px;
|
|
4473
4491
|
font-weight: 700;
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
.subagent-reply-body {
|
|
4477
|
-
display: inline;
|
|
4492
|
+
line-height: 1.55;
|
|
4478
4493
|
}
|
|
4479
4494
|
|
|
4480
4495
|
.subagent-reply pre,
|
|
@@ -4495,98 +4510,250 @@
|
|
|
4495
4510
|
min-width: 0;
|
|
4496
4511
|
}
|
|
4497
4512
|
|
|
4498
|
-
/* ===== subagent
|
|
4499
|
-
|
|
4500
|
-
expanded
|
|
4501
|
-
|
|
4502
|
-
.subagent-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
.subagent-reply-scroll {
|
|
4513
|
+
/* ===== subagent panel(整段输出统一折叠面板)=====
|
|
4514
|
+
结构:header(handoff + 展开按钮)+ body(max-height 22em 滚动)+ footer(同款按钮)
|
|
4515
|
+
状态:data-expanded="false" 预览 / "true" 展开(body 拉到 70vh 仍可滚)
|
|
4516
|
+
色彩:跟随 --agent-color(外层 inline style 注入),同 agent 跨 turn 颜色稳定 */
|
|
4517
|
+
.subagent-panel {
|
|
4518
|
+
margin: 8px 0;
|
|
4519
|
+
border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 25%, transparent);
|
|
4520
|
+
border-left: 3px solid color-mix(in srgb, var(--agent-color, var(--accent)) 55%, transparent);
|
|
4521
|
+
border-radius: 10px;
|
|
4509
4522
|
overflow: hidden;
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
max-width: 100%;
|
|
4523
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 3%, var(--bg-surface));
|
|
4524
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
4513
4525
|
}
|
|
4514
|
-
.subagent-reply-scroll > :first-child { margin-top: 0; }
|
|
4515
|
-
.subagent-reply-scroll > :last-child { margin-bottom: 0; }
|
|
4516
4526
|
|
|
4517
|
-
.subagent-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4527
|
+
.subagent-panel-header {
|
|
4528
|
+
display: flex;
|
|
4529
|
+
align-items: center;
|
|
4530
|
+
gap: 8px;
|
|
4531
|
+
padding: 8px 12px;
|
|
4532
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 10%, var(--bg-surface));
|
|
4533
|
+
border-bottom: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 16%, transparent);
|
|
4534
|
+
cursor: pointer;
|
|
4535
|
+
user-select: none;
|
|
4536
|
+
font-size: 0.78rem;
|
|
4537
|
+
line-height: 1.35;
|
|
4538
|
+
transition: background 0.15s ease;
|
|
4539
|
+
}
|
|
4540
|
+
.subagent-panel-header:hover {
|
|
4541
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 14%, var(--bg-surface));
|
|
4542
|
+
}
|
|
4543
|
+
.subagent-panel-header:focus-visible {
|
|
4544
|
+
outline: 2px solid color-mix(in srgb, var(--agent-color, var(--accent)) 55%, transparent);
|
|
4545
|
+
outline-offset: -2px;
|
|
4522
4546
|
}
|
|
4523
4547
|
|
|
4524
|
-
.subagent-
|
|
4525
|
-
|
|
4526
|
-
|
|
4548
|
+
.subagent-panel-avatar {
|
|
4549
|
+
flex: 0 0 auto;
|
|
4550
|
+
width: 22px;
|
|
4551
|
+
height: 22px;
|
|
4552
|
+
border-radius: 5px;
|
|
4553
|
+
overflow: hidden;
|
|
4554
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 18%, transparent);
|
|
4555
|
+
display: inline-flex;
|
|
4556
|
+
align-items: center;
|
|
4557
|
+
justify-content: center;
|
|
4558
|
+
}
|
|
4559
|
+
.subagent-panel-avatar svg,
|
|
4560
|
+
.subagent-panel-avatar img {
|
|
4561
|
+
width: 100%;
|
|
4562
|
+
height: 100%;
|
|
4563
|
+
display: block;
|
|
4527
4564
|
}
|
|
4528
4565
|
|
|
4529
|
-
.subagent-
|
|
4530
|
-
|
|
4531
|
-
|
|
4566
|
+
.subagent-panel-arrow {
|
|
4567
|
+
flex: 0 0 auto;
|
|
4568
|
+
color: color-mix(in srgb, var(--agent-color, var(--accent)) 65%, var(--text-primary));
|
|
4569
|
+
opacity: 0.8;
|
|
4570
|
+
font-weight: 600;
|
|
4532
4571
|
}
|
|
4533
|
-
|
|
4534
|
-
|
|
4572
|
+
|
|
4573
|
+
.subagent-panel-attribution {
|
|
4574
|
+
flex: 1 1 auto;
|
|
4575
|
+
min-width: 0;
|
|
4576
|
+
color: var(--text-primary);
|
|
4535
4577
|
overflow: hidden;
|
|
4536
|
-
|
|
4578
|
+
text-overflow: ellipsis;
|
|
4579
|
+
white-space: nowrap;
|
|
4580
|
+
}
|
|
4581
|
+
.subagent-panel-name {
|
|
4582
|
+
color: color-mix(in srgb, var(--agent-color, var(--accent)) 88%, var(--text-primary));
|
|
4583
|
+
font-weight: 600;
|
|
4584
|
+
}
|
|
4585
|
+
.subagent-panel-tag {
|
|
4586
|
+
display: inline-block;
|
|
4587
|
+
margin: 0 6px;
|
|
4588
|
+
padding: 0 6px;
|
|
4589
|
+
font-size: 0.62rem;
|
|
4590
|
+
font-weight: 500;
|
|
4591
|
+
letter-spacing: 0.04em;
|
|
4592
|
+
color: var(--agent-color, var(--accent));
|
|
4593
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 12%, transparent);
|
|
4594
|
+
border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 30%, transparent);
|
|
4595
|
+
border-radius: 4px;
|
|
4596
|
+
vertical-align: 1px;
|
|
4597
|
+
line-height: 1.4;
|
|
4598
|
+
white-space: nowrap;
|
|
4599
|
+
/* 注意:不要加 text-transform: uppercase。之前用 lowercase "subagent" + uppercase 强转
|
|
4600
|
+
出来 "SUBAGENT",结果中文 UI 里冒出来一个英文大写词跟用户配置语言(中文)对不上。
|
|
4601
|
+
现在文案直接走 i18n(t("subagent.tag") = "子代理" / "Subagent"),无需大小写强制。 */
|
|
4602
|
+
}
|
|
4603
|
+
.subagent-panel-task-desc {
|
|
4604
|
+
color: var(--text-muted);
|
|
4605
|
+
font-weight: 400;
|
|
4537
4606
|
}
|
|
4538
4607
|
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
position: absolute;
|
|
4542
|
-
right: 8px;
|
|
4543
|
-
bottom: 6px;
|
|
4608
|
+
.subagent-panel-toggle {
|
|
4609
|
+
flex: 0 0 auto;
|
|
4544
4610
|
display: inline-flex;
|
|
4545
4611
|
align-items: center;
|
|
4546
|
-
gap:
|
|
4547
|
-
padding:
|
|
4612
|
+
gap: 4px;
|
|
4613
|
+
padding: 3px 10px;
|
|
4548
4614
|
font-size: 0.7rem;
|
|
4549
4615
|
font-weight: 500;
|
|
4550
|
-
line-height: 1.
|
|
4551
|
-
color: var(--accent);
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
background: color-mix(in srgb, var(--agent-color, var(--accent)) 8%, var(--bg-surface, #fff));
|
|
4555
|
-
border: 1px solid rgba(197, 101, 61, 0.28);
|
|
4556
|
-
border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 28%, transparent);
|
|
4616
|
+
line-height: 1.3;
|
|
4617
|
+
color: color-mix(in srgb, var(--agent-color, var(--accent)) 78%, var(--text-primary));
|
|
4618
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 8%, var(--bg-surface));
|
|
4619
|
+
border: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 30%, transparent);
|
|
4557
4620
|
border-radius: 999px;
|
|
4558
4621
|
cursor: pointer;
|
|
4559
4622
|
user-select: none;
|
|
4560
|
-
transition: background 0.15s ease,
|
|
4623
|
+
transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
|
|
4561
4624
|
}
|
|
4562
|
-
.subagent-
|
|
4563
|
-
background:
|
|
4564
|
-
background: color-mix(in srgb, var(--agent-color, var(--accent)) 16%, var(--bg-surface, #fff));
|
|
4565
|
-
border-color: rgba(197, 101, 61, 0.45);
|
|
4625
|
+
.subagent-panel-toggle:hover {
|
|
4626
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 18%, var(--bg-surface));
|
|
4566
4627
|
border-color: color-mix(in srgb, var(--agent-color, var(--accent)) 45%, transparent);
|
|
4567
4628
|
}
|
|
4568
|
-
.subagent-
|
|
4629
|
+
.subagent-panel-toggle:active {
|
|
4569
4630
|
transform: translateY(1px);
|
|
4570
4631
|
}
|
|
4571
|
-
.subagent-
|
|
4572
|
-
|
|
4573
|
-
|
|
4632
|
+
.subagent-panel-toggle-icon {
|
|
4633
|
+
transition: transform 0.2s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
4634
|
+
}
|
|
4635
|
+
.subagent-panel[data-expanded="true"] .subagent-panel-toggle-icon {
|
|
4636
|
+
transform: rotate(180deg);
|
|
4574
4637
|
}
|
|
4575
4638
|
|
|
4576
|
-
/*
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4639
|
+
/* body:默认 max-height 22em(≈14 行),内部滚动;展开后拉到 70vh 仍内部可滚,
|
|
4640
|
+
避免一个超长 subagent 把整页撑爆。展开过渡只动 max-height,body 内的卡片
|
|
4641
|
+
不动,不会有抖动。 */
|
|
4642
|
+
.subagent-panel-body {
|
|
4643
|
+
padding: 10px 14px;
|
|
4644
|
+
overflow-y: auto;
|
|
4645
|
+
overflow-x: hidden;
|
|
4646
|
+
min-width: 0;
|
|
4647
|
+
max-width: 100%;
|
|
4648
|
+
transition: max-height 0.28s var(--ease-out-expo, cubic-bezier(0.16, 1, 0.3, 1));
|
|
4649
|
+
scrollbar-gutter: stable;
|
|
4580
4650
|
}
|
|
4581
|
-
.subagent-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4651
|
+
.subagent-panel[data-expanded="false"] .subagent-panel-body {
|
|
4652
|
+
max-height: 22em;
|
|
4653
|
+
}
|
|
4654
|
+
.subagent-panel[data-expanded="true"] .subagent-panel-body {
|
|
4655
|
+
max-height: 70vh;
|
|
4656
|
+
}
|
|
4657
|
+
.subagent-panel-body > :first-child { margin-top: 0; }
|
|
4658
|
+
.subagent-panel-body > :last-child { margin-bottom: 0; }
|
|
4659
|
+
|
|
4660
|
+
/* body 里的工具卡 / 文本:缩一点字号 + 减小 margin,让 22em 能装下更多内容 */
|
|
4661
|
+
.subagent-panel-body .tool-use-card,
|
|
4662
|
+
.subagent-panel-body .tool-group,
|
|
4663
|
+
.subagent-panel-body .inline-tool,
|
|
4664
|
+
.subagent-panel-body .inline-terminal,
|
|
4665
|
+
.subagent-panel-body .inline-diff {
|
|
4666
|
+
margin: 4px 0;
|
|
4667
|
+
}
|
|
4668
|
+
.subagent-panel-body .markdown-content {
|
|
4669
|
+
font-size: 0.875rem;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
/* body 滚动条:弱化但仍可见,对应 agent 色 */
|
|
4673
|
+
.subagent-panel-body::-webkit-scrollbar {
|
|
4674
|
+
width: 7px;
|
|
4675
|
+
height: 7px;
|
|
4676
|
+
}
|
|
4677
|
+
.subagent-panel-body::-webkit-scrollbar-thumb {
|
|
4678
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 32%, transparent);
|
|
4679
|
+
border-radius: 4px;
|
|
4585
4680
|
}
|
|
4586
|
-
.subagent-
|
|
4681
|
+
.subagent-panel-body::-webkit-scrollbar-thumb:hover {
|
|
4682
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 50%, transparent);
|
|
4683
|
+
}
|
|
4684
|
+
.subagent-panel-body::-webkit-scrollbar-track {
|
|
4587
4685
|
background: transparent;
|
|
4588
4686
|
}
|
|
4589
4687
|
|
|
4688
|
+
/* footer:跟 header 对称,永远可见在 panel 底部。滚到 body 最下时用户能就近点。
|
|
4689
|
+
hint 是个软提示"— 协作猫 —",告诉用户 panel 边界,避免与下一段父消息混淆。 */
|
|
4690
|
+
.subagent-panel-footer {
|
|
4691
|
+
display: flex;
|
|
4692
|
+
align-items: center;
|
|
4693
|
+
gap: 10px;
|
|
4694
|
+
padding: 6px 12px;
|
|
4695
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 6%, var(--bg-surface));
|
|
4696
|
+
border-top: 1px solid color-mix(in srgb, var(--agent-color, var(--accent)) 14%, transparent);
|
|
4697
|
+
font-size: 0.7rem;
|
|
4698
|
+
}
|
|
4699
|
+
.subagent-panel-footer-hint {
|
|
4700
|
+
flex: 1 1 auto;
|
|
4701
|
+
min-width: 0;
|
|
4702
|
+
color: var(--text-muted);
|
|
4703
|
+
opacity: 0.7;
|
|
4704
|
+
font-family: var(--font-mono);
|
|
4705
|
+
letter-spacing: 0.04em;
|
|
4706
|
+
overflow: hidden;
|
|
4707
|
+
text-overflow: ellipsis;
|
|
4708
|
+
white-space: nowrap;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
/* ===== panel 内最终回复的简化标记块(外层 panel 已经管折叠,这里只剩"任务完成"标记)===== */
|
|
4712
|
+
.subagent-reply.collapsible {
|
|
4713
|
+
padding: 0;
|
|
4714
|
+
overflow: hidden;
|
|
4715
|
+
}
|
|
4716
|
+
.subagent-reply.final {
|
|
4717
|
+
margin: 6px 0 0;
|
|
4718
|
+
padding: 10px 12px;
|
|
4719
|
+
border-radius: 8px;
|
|
4720
|
+
border-left: 3px solid color-mix(in srgb, var(--agent-color, var(--accent)) 60%, transparent);
|
|
4721
|
+
background: color-mix(in srgb, var(--agent-color, var(--accent)) 5%, var(--bg-surface));
|
|
4722
|
+
}
|
|
4723
|
+
.subagent-reply.final.error {
|
|
4724
|
+
border-left-color: var(--danger, #e2574c);
|
|
4725
|
+
background: color-mix(in srgb, var(--danger, #e2574c) 5%, var(--bg-surface));
|
|
4726
|
+
}
|
|
4727
|
+
.subagent-reply-marker {
|
|
4728
|
+
display: inline-flex;
|
|
4729
|
+
align-items: center;
|
|
4730
|
+
gap: 4px;
|
|
4731
|
+
margin-bottom: 4px;
|
|
4732
|
+
font-size: 0.68rem;
|
|
4733
|
+
font-weight: 600;
|
|
4734
|
+
letter-spacing: 0.04em;
|
|
4735
|
+
text-transform: uppercase;
|
|
4736
|
+
color: color-mix(in srgb, var(--agent-color, var(--accent)) 78%, var(--text-primary));
|
|
4737
|
+
}
|
|
4738
|
+
.subagent-reply-marker.error {
|
|
4739
|
+
color: var(--danger, #e2574c);
|
|
4740
|
+
}
|
|
4741
|
+
.subagent-reply-marker.pending {
|
|
4742
|
+
color: var(--text-muted);
|
|
4743
|
+
text-transform: none;
|
|
4744
|
+
letter-spacing: 0;
|
|
4745
|
+
font-weight: 500;
|
|
4746
|
+
}
|
|
4747
|
+
.subagent-reply-marker-icon {
|
|
4748
|
+
font-weight: 700;
|
|
4749
|
+
}
|
|
4750
|
+
.subagent-reply.final .subagent-reply-content > :first-child { margin-top: 0; }
|
|
4751
|
+
.subagent-reply.final .subagent-reply-content > :last-child { margin-bottom: 0; }
|
|
4752
|
+
|
|
4753
|
+
/* (旧的 .subagent-reply-header / .subagent-reply-toggle / .subagent-reply-body /
|
|
4754
|
+
.subagent-reply-fade / .subagent-reply-body-inline 系列已移除——现在外层
|
|
4755
|
+
.subagent-panel 接管整段折叠 + 滚动,内层最终回复退化为简化标记块。) */
|
|
4756
|
+
|
|
4590
4757
|
/* ===== 消息使用量信息 ===== */
|
|
4591
4758
|
.message-usage {
|
|
4592
4759
|
margin-top: 8px;
|
|
@@ -10723,6 +10890,26 @@
|
|
|
10723
10890
|
.tool-use-file { max-width: 100%; }
|
|
10724
10891
|
.tool-use-body { padding: 10px 12px; }
|
|
10725
10892
|
.subagent-reply { padding: 9px 12px; }
|
|
10893
|
+
.subagent-reply.collapsible { padding: 0; }
|
|
10894
|
+
.subagent-reply.final { padding: 9px 11px; }
|
|
10895
|
+
/* subagent-panel:窄屏头部不省略 attribution(可换行多行 vs 单行省略),
|
|
10896
|
+
body 高度收紧一点,footer hint 隐掉只留按钮。 */
|
|
10897
|
+
.subagent-panel { margin: 6px 0; }
|
|
10898
|
+
.subagent-panel-header {
|
|
10899
|
+
padding: 7px 10px;
|
|
10900
|
+
gap: 6px;
|
|
10901
|
+
font-size: 0.74rem;
|
|
10902
|
+
}
|
|
10903
|
+
.subagent-panel-attribution {
|
|
10904
|
+
white-space: normal;
|
|
10905
|
+
line-height: 1.4;
|
|
10906
|
+
}
|
|
10907
|
+
.subagent-panel-tag { margin: 0 4px; padding: 0 4px; }
|
|
10908
|
+
.subagent-panel-body { padding: 9px 11px; }
|
|
10909
|
+
.subagent-panel[data-expanded="false"] .subagent-panel-body { max-height: 18em; }
|
|
10910
|
+
.subagent-panel[data-expanded="true"] .subagent-panel-body { max-height: 60vh; }
|
|
10911
|
+
.subagent-panel-footer { padding: 5px 10px; }
|
|
10912
|
+
.subagent-panel-footer-hint { display: none; }
|
|
10726
10913
|
.markdown-content .code-block { margin: 8px 0; }
|
|
10727
10914
|
.markdown-content pre,
|
|
10728
10915
|
.markdown-content .code-block pre,
|
package/dist/web-ui/styles.js
CHANGED
|
@@ -2,12 +2,26 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
//
|
|
5
|
+
// 用 mtime 做缓存键,磁盘上 CSS 一变(npm run build / 手工 edit dist/)下次请求
|
|
6
|
+
// 就会自动 re-read。否则进程启动时缓存的 CSS 会粘住整个生命周期,UI 改动看不到效果,
|
|
7
|
+
// 必须重启 wand 才能生效——开发 / 修 UI 的时候这点尤其难受。
|
|
8
|
+
// 同步 stat 的成本:本地 fs,~几十微秒,相对一次 HTML 渲染可忽略。
|
|
6
9
|
let _cssCache = null;
|
|
10
|
+
let _cssCacheMtimeMs = 0;
|
|
7
11
|
export function getCSSStyles() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const cssPath = path.join(__dirname, "content", "styles.css");
|
|
13
|
+
try {
|
|
14
|
+
const stat = fs.statSync(cssPath);
|
|
15
|
+
if (_cssCache === null || stat.mtimeMs !== _cssCacheMtimeMs) {
|
|
16
|
+
_cssCache = fs.readFileSync(cssPath, "utf-8");
|
|
17
|
+
_cssCacheMtimeMs = stat.mtimeMs;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// 文件丢了就退化到旧缓存(如果有),还没缓存过就抛出原错误让 server 知道。
|
|
22
|
+
if (_cssCache === null) {
|
|
23
|
+
_cssCache = fs.readFileSync(cssPath, "utf-8");
|
|
24
|
+
}
|
|
11
25
|
}
|
|
12
26
|
return _cssCache;
|
|
13
27
|
}
|