@yxzpro/agent-acta 2.14.4

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/LOGFORMATS.md +960 -0
  3. package/README.md +238 -0
  4. package/REFERENCE.md +1428 -0
  5. package/agent-acta-page.html +1998 -0
  6. package/agent-acta-server.mjs +1066 -0
  7. package/agent-acta-widget.html +724 -0
  8. package/archive.mjs +335 -0
  9. package/core/log.mjs +20 -0
  10. package/core/service.mjs +4285 -0
  11. package/hooks/cursor-usage-hook.mjs +89 -0
  12. package/mcp-server.mjs +372 -0
  13. package/package.json +68 -0
  14. package/page/dialog-add-agent.js +65 -0
  15. package/page/dialog-alert.js +159 -0
  16. package/page/dialog-archive.js +342 -0
  17. package/page/dialog-compare.js +238 -0
  18. package/page/dialog-ctxw.js +101 -0
  19. package/page/dialog-diag.js +72 -0
  20. package/page/dialog-heatmap.js +215 -0
  21. package/page/dialog-selftest.js +202 -0
  22. package/page/dialog-sess-timeline.js +108 -0
  23. package/page/dialog-stats.js +694 -0
  24. package/page/dialog-usage.js +158 -0
  25. package/page/log-card.js +277 -0
  26. package/page/log-view.js +273 -0
  27. package/page/sess-view.js +382 -0
  28. package/page/shared.js +408 -0
  29. package/page/sidebar.js +129 -0
  30. package/page/style.css +973 -0
  31. package/page/topbar.js +61 -0
  32. package/parsers/atomcode.mjs +321 -0
  33. package/parsers/buddy.mjs +272 -0
  34. package/parsers/buddyext.mjs +582 -0
  35. package/parsers/claude.mjs +496 -0
  36. package/parsers/cline.mjs +468 -0
  37. package/parsers/codearts.mjs +96 -0
  38. package/parsers/codex.mjs +188 -0
  39. package/parsers/comate.mjs +195 -0
  40. package/parsers/copilot.mjs +308 -0
  41. package/parsers/cursor.mjs +493 -0
  42. package/parsers/devindb.mjs +358 -0
  43. package/parsers/discovery.mjs +1230 -0
  44. package/parsers/doubao.mjs +406 -0
  45. package/parsers/dsh.mjs +377 -0
  46. package/parsers/gemini.mjs +267 -0
  47. package/parsers/generic.mjs +494 -0
  48. package/parsers/hermes.mjs +398 -0
  49. package/parsers/kilo.mjs +42 -0
  50. package/parsers/kimi.mjs +249 -0
  51. package/parsers/mimocode.mjs +531 -0
  52. package/parsers/minimax.mjs +503 -0
  53. package/parsers/openclaw.mjs +490 -0
  54. package/parsers/opencode.mjs +465 -0
  55. package/parsers/shared.mjs +418 -0
  56. package/parsers/trace.mjs +66 -0
  57. package/parsers/tracecode.mjs +348 -0
  58. package/parsers/traedb.mjs +528 -0
  59. package/parsers/zcode.mjs +356 -0
  60. package/plugin/client.js +51 -0
  61. package/plugin/cordis.patch.yml +5 -0
  62. package/plugin/plugin.mjs +220 -0
  63. package/search.mjs +554 -0
  64. package/tools/grab-trae-key.ps1 +265 -0
  65. package/vendor/element-plus-icons.js +3 -0
  66. package/vendor/element-plus.css +1 -0
  67. package/vendor/element-plus.dark.css +1 -0
  68. package/vendor/element-plus.full.min.js +65 -0
  69. package/vendor/element-plus.zh-cn.js +2 -0
  70. package/vendor/svg/antgroup-color.svg +1 -0
  71. package/vendor/svg/bytedance-color.svg +1 -0
  72. package/vendor/svg/chatglm-color.svg +1 -0
  73. package/vendor/svg/claude-color.svg +1 -0
  74. package/vendor/svg/claudecode-color.svg +1 -0
  75. package/vendor/svg/cline.svg +1 -0
  76. package/vendor/svg/codebuddy-color.svg +1 -0
  77. package/vendor/svg/codebuddy.svg +1 -0
  78. package/vendor/svg/codex-color.svg +1 -0
  79. package/vendor/svg/copilot.svg +1 -0
  80. package/vendor/svg/cursor.svg +1 -0
  81. package/vendor/svg/deepseek-color.svg +1 -0
  82. package/vendor/svg/deepseek-harness-whale.svg +8 -0
  83. package/vendor/svg/doubao-color.svg +1 -0
  84. package/vendor/svg/gemini-color.svg +1 -0
  85. package/vendor/svg/glmv-color.svg +1 -0
  86. package/vendor/svg/grok.svg +1 -0
  87. package/vendor/svg/hermesagent.svg +1 -0
  88. package/vendor/svg/huawei-color.svg +1 -0
  89. package/vendor/svg/hunyuan-color.svg +1 -0
  90. package/vendor/svg/kilocode-avatar.svg +1 -0
  91. package/vendor/svg/kilocode.svg +1 -0
  92. package/vendor/svg/kimi-color.svg +1 -0
  93. package/vendor/svg/kimi.svg +1 -0
  94. package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
  95. package/vendor/svg/logo-AtomGit-dark.svg +4 -0
  96. package/vendor/svg/logo-AtomGit-light.svg +4 -0
  97. package/vendor/svg/meta-color.svg +1 -0
  98. package/vendor/svg/mimo.svg +66 -0
  99. package/vendor/svg/minimax-code-logo.svg +8 -0
  100. package/vendor/svg/minimax-color.svg +1 -0
  101. package/vendor/svg/moonshot.svg +1 -0
  102. package/vendor/svg/nex-color.svg +1 -0
  103. package/vendor/svg/nex.svg +1 -0
  104. package/vendor/svg/nvidia-color.svg +1 -0
  105. package/vendor/svg/openai.svg +1 -0
  106. package/vendor/svg/openclaw-color.svg +1 -0
  107. package/vendor/svg/openclaw.svg +1 -0
  108. package/vendor/svg/opencode.svg +1 -0
  109. package/vendor/svg/openrouter-color.svg +1 -0
  110. package/vendor/svg/qoder-color.svg +1 -0
  111. package/vendor/svg/qwen-color.svg +1 -0
  112. package/vendor/svg/sensenova-color.svg +1 -0
  113. package/vendor/svg/stepfun.svg +1 -0
  114. package/vendor/svg/trae-color.svg +1 -0
  115. package/vendor/svg/traework-v2.svg +1 -0
  116. package/vendor/svg/wenxin-color.svg +1 -0
  117. package/vendor/svg/windsurf.svg +1 -0
  118. package/vendor/svg/workbuddy-color.svg +29 -0
  119. package/vendor/svg/xiaomimimo.svg +1 -0
  120. package/vendor/svg/yuanbao-color.svg +1 -0
  121. package/vendor/svg/yuanbao.svg +1 -0
  122. package/vendor/svg/zai.svg +1 -0
  123. package/vendor/svg/zhipu-color.svg +1 -0
  124. package/vendor/vue.global.prod.js +9 -0
  125. package/widget/main.js +202 -0
  126. package/widget/package.json +7 -0
  127. package/widget/preload.js +11 -0
package/page/shared.js ADDED
@@ -0,0 +1,408 @@
1
+ // 页面共享的纯函数与常量(R25 批 0 从 agent-acta-page.html 的根内联 script 搬出的第一块)。
2
+ //
3
+ // 形态:经典 script(不是 ESM)—— 顶层声明即全局,后面的根内联 script 与其模板能直接看见。
4
+ // 约束(拆的时候定死,后面每批照抄):
5
+ // · 这里只放**不依赖响应式状态**的东西;用到 agentsMeta / liveIds / details 等 ref / reactive 的一律留在 setup。
6
+ // (所以 toolJsonFold / callOpen / isLive / titleOf / isDisabled / pathTip 这些没搬 —— 它们吃着 setup 里的状态。)
7
+ // · **不得**重新声明 projNorm / projKey / rangeDayStart —— 唯一来源是服务端 SHARED_JS 注入(R12),
8
+ // test/shared-snippet-test.mjs 与 test/page-components-test.mjs 都会拦。
9
+ // · 模板只能看见 setup() return 里的键:根 script 的 return 仍逐个列出这里的函数名。
10
+ // 别指望模板直接去找全局 —— prod Vue 的 _ctx 代理对未知名字一律返回 undefined,表现是那格静默空白。
11
+ // 加载顺序:vendor(Vue / Element Plus)→ 本文件 → SHARED_JS 注入段 → 各组件文件 → 根内联 script。
12
+ // ⚠ 顶层 const / function 与根 script 的顶层声明共享同一个全局词法作用域:重名 = 整页 SyntaxError。
13
+ // 跨文件重名由 test/page-components-test.mjs 静态拦;本文件刻意不声明任何 ElMessage / Vue 解构名。
14
+ //
15
+ // ---- R25 组件文件契约(**机器可定位**,test/page-check.mjs 与 test/page-components-test.mjs 按此抠键)----
16
+ // 每个组件文件(page/*.js 里除本文件外的都是)必须长成这个形状,不许多态:
17
+ //
18
+ // window.AACTA = window.AACTA || {};
19
+ // AACTA.XxxView = {
20
+ // name: 'aa-xxx', // 必须 aa- 前缀(全局注册防撞 + 检查器识别锚点)
21
+ // template: `<div>…</div>`, // 反引号字符串;**不得**出现未转义的 `${`(会变成 JS 插值)
22
+ // props: { a: …, b: … }, // 对象字面量或字符串数组两种形态都行
23
+ // inject: ['padLeft'], // 同上
24
+ // emits: ['open-session'],
25
+ // setup(props, { emit }) {
26
+ // …
27
+ // return { x, y }; // 必须是函数体里**最后一个**顶层 return,且是对象字面量
28
+ // }
29
+ // };
30
+ //
31
+ // 另外两条风格约束(检查器靠它们收名字,破了不会报错但检查会失明):
32
+ // · 顶层声明一律**顶格**(不缩进)—— page/*.js 的顶层名集合由顶格正则收集;
33
+ // · 一个文件恰一个组件(`AACTA.<ID> = {` 只许出现一处)。
34
+ // 注册方式(方案 C 拍板):组件不自己 app.component,统一由根内联 script 末尾注册(时序天然安全)。
35
+
36
+ // 品牌色 / 品牌图标映射(图标来自 @lobehub/icons 静态 SVG,已本地化到 /vendor/svg/)
37
+ const AGENT_COLORS = { atomcode: '#3b74e7', codebuddy: '#18a058', 'codebuddy-ext': '#18a058', workbuddy: '#7c5cd6', claude: '#d97757', zcode: '#0e9aa7' };
38
+ const PALETTE = ['#3b74e7', '#18a058', '#7c5cd6', '#c26a1b', '#c2185b', '#0e9aa7', '#795548'];
39
+ const AGENT_ICONS = { atomcode: 'logo-AtomGit-G-red.svg', claude: 'claudecode-color.svg', codebuddy: 'codebuddy-color.svg', 'codebuddy-ext': 'codebuddy-color.svg', workbuddy: 'workbuddy-color.svg', trae: 'trae-color.svg', traework: 'traework-v2.svg', cursor: 'cursor.svg', codex: 'codex-color.svg', opencode: 'opencode.svg', kilo: 'kilocode.svg', openclaw: 'openclaw-color.svg', cline: 'cline.svg', qoder: 'qoder-color.svg', 'qoder-cn': 'qoder-color.svg', codeart: 'huawei-color.svg', codearts: 'huawei-color.svg', gemini: 'gemini-color.svg', kimi: 'kimi.svg', 'zcode': 'zai.svg', 'dsh': 'deepseek-harness-whale.svg', hermes: 'hermesagent.svg', devin: 'windsurf.svg', qwen: 'qwen-color.svg', doubao: 'doubao-color.svg', hunyuan: 'hunyuan-color.svg', grok: 'grok.svg', copilot: 'copilot.svg', windsurf: 'windsurf.svg', minimax: 'minimax-code-logo.svg', mimocode: 'mimo.svg' };
40
+ // R16 遗留子项:只有图标、后端零检测的名字 —— 不会自动发现、没有解析器。
41
+ // 用户在「添加 agent」里敲这些名字时会误以为能自动识别,必须明示(口径与需求书 R16 一致)。
42
+ // doubao 已有解析器(R26)、hermes 已于 2026-09-21 接入(state.db SQLite + agent.log 辅源)、
43
+ // openclaw 已于 2026-09-23 接入(~/.openclaw/agents/<id>/sessions/*.jsonl 追加式会话树),从名单里移除(图标仍在 AGENT_ICONS,正常显示)
44
+ // devin 已于 2026-09-24 接入(%APPDATA%\Devin\cli\sessions.db 明文 SQLite)。图标**沿用 windsurf.svg**:
45
+ // 装的这份 Devin 桌面版里,产品自己的扩展目录就叫 `extensions/windsurf`、displayName 写的是 "Devin"
46
+ // (库里 backend_type 也是 'windsurf')—— 同一家同一个产品,本机拿不到单独的 Devin 品牌图。
47
+ // 将来手上有官方 devin.svg 时,放进 vendor/svg/ 再把这里改成 devin.svg 即可。
48
+ // kilo 已于 2026-09-24 接入(~/.local/share/kilo/kilo.db,opencode 的 fork);图标用已有的 kilocode.svg。
49
+ const ICON_ONLY_AGENTS = new Set(['qwen', 'hunyuan', 'grok']);
50
+ const MODEL_ICONS = [
51
+ ['claude', 'claude-color.svg'],
52
+ ['glm', 'zhipu-color.svg'],
53
+ ['deepseek', 'deepseek-color.svg'],
54
+ ['qwen', 'qwen-color.svg'],
55
+ ['codex', 'codex-color.svg'],
56
+ ['gpt', 'openai.svg'], ['openai', 'openai.svg'],
57
+ ['gemini', 'gemini-color.svg'],
58
+ ['grok', 'grok.svg'],
59
+ ['kimi', 'kimi.svg'], ['moonshot', 'moonshot.svg'],
60
+ ['doubao', 'doubao-color.svg'],
61
+ ['minimax', 'minimax-color.svg'],
62
+ ['hunyuan', 'hunyuan-color.svg'],
63
+ ['wenxin', 'wenxin-color.svg'], ['ernie', 'wenxin-color.svg'],
64
+ ['mimo', 'xiaomimimo.svg'],
65
+ ['yuanbao', 'yuanbao-color.svg'], ['bytedance', 'bytedance-color.svg'], ['pangu', 'huawei-color.svg'],
66
+ ['mai-code', 'copilot.svg'],
67
+ ];
68
+
69
+ const fmtN = n => !n ? '0' : (n >= 1e6 ? (n/1e6).toFixed(1)+'M' : (n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n)));
70
+ const fmtDur = ms => !ms ? '—' : ms < 1000 ? ms+'ms' : ms < 60000 ? (ms/1000).toFixed(1)+'s' : Math.floor(ms/60000)+'m'+Math.round(ms%60000/1000)+'s';
71
+ // qoder 按 credits 计费、token 恒为 0,卡片改显真实用量(积分 / 上下文占比 / 压缩前后 token)。
72
+ // 解析器只对 qoder 落这几个字段,故用「字段存在」判定即可,不必硬编码 agent 名单。
73
+ const isQoder = e => e.credits != null;
74
+ const fmtCredits = n => !n ? '0' : (n >= 100 ? n.toFixed(0) : n.toFixed(2));
75
+ const fmtPct = r => !r ? '0%' : (r <= 1 ? (r * 100).toFixed(1) : r.toFixed(1)) + '%';
76
+
77
+ // ---- R3 详情渲染辅助(全本地实现,不引外部库:页面是离线本地服务,CDN 不可用)----
78
+ const escHtml = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
79
+ // 简易代码高亮:先转义再上色(顺序不能反,否则会被注入)。覆盖常见关键字/字符串/注释/数字,
80
+ // 目的是「可读」而不是「完备」—— 渲染错漏不影响原文(复制按钮给的是原文)。
81
+ function hlCode(code) {
82
+ let s = escHtml(code);
83
+ s = s.replace(/(&quot;[^&]*?&quot;|&#39;[^&]*?&#39;|`[^`]*?`)/g, '<span class="hl-str">$1</span>');
84
+ s = s.replace(/(^|\n)(\s*)(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/)/g, '$1$2<span class="hl-com">$3</span>');
85
+ s = s.replace(/\b(const|let|var|function|return|if|else|for|while|class|new|import|from|export|async|await|try|catch|throw|def|self|this|null|true|false|undefined|None|True|False)\b/g, '<span class="hl-kw">$1</span>');
86
+ s = s.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span class="hl-num">$1</span>');
87
+ return s;
88
+ }
89
+ // 轻量 Markdown → HTML:块级(标题/列表/代码围栏/引用/分隔线/段落)+ 行内(粗斜体/行内码/链接)。
90
+ // 输入一律先 escHtml,任何未识别语法都原样呈现,绝不拼原文进 HTML。
91
+ function renderMd(src) {
92
+ const text = String(src ?? '');
93
+ const lines = text.split('\n');
94
+ let out = '', i = 0, para = [];
95
+ const flush = () => { if (para.length) { out += '<p>' + inlineMd(escHtml(para.join('\n'))).replace(/\n/g, '<br>') + '</p>'; para = []; } };
96
+ function inlineMd(s) {
97
+ return s
98
+ .replace(/`([^`]+)`/g, (_, c) => '<code class="md-ic">' + c + '</code>')
99
+ .replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
100
+ .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<i>$2</i>')
101
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
102
+ }
103
+ while (i < lines.length) {
104
+ const L = lines[i];
105
+ if (/^```/.test(L)) {
106
+ flush();
107
+ const lang = L.slice(3).trim();
108
+ let j = i + 1, buf = [];
109
+ while (j < lines.length && !/^```/.test(lines[j])) buf.push(lines[j++]);
110
+ out += '<pre class="md-code">' + (lang ? '<span class="hl-lang">' + escHtml(lang) + '</span>' : '') + '<code>' + hlCode(buf.join('\n')) + '</code></pre>';
111
+ i = j + 1; continue;
112
+ }
113
+ const h = L.match(/^(#{1,4})\s+(.*)/);
114
+ if (h) { flush(); out += '<h5 class="md-h">' + inlineMd(escHtml(h[2])) + '</h5>'; i++; continue; }
115
+ if (/^\s*([-*+]|\d+\.)\s+/.test(L)) {
116
+ flush();
117
+ const items = [];
118
+ while (i < lines.length && /^\s*([-*+]|\d+\.)\s+/.test(lines[i])) items.push(lines[i].replace(/^\s*([-*+]|\d+\.)\s+/, '')), i++;
119
+ out += '<ul class="md-ul">' + items.map(t => '<li>' + inlineMd(escHtml(t)) + '</li>').join('') + '</ul>';
120
+ continue;
121
+ }
122
+ if (/^>\s?/.test(L)) { flush(); out += '<blockquote class="md-q">' + inlineMd(escHtml(L.replace(/^>\s?/, ''))) + '</blockquote>'; i++; continue; }
123
+ if (/^\s*(-{3,}|\*{3,})\s*$/.test(L)) { flush(); out += '<hr class="md-hr">'; i++; continue; }
124
+ if (!L.trim()) { flush(); i++; continue; }
125
+ para.push(L); i++;
126
+ }
127
+ flush();
128
+ return out || '<p>(空)</p>';
129
+ }
130
+ // 工具入参/返回:能解析成 JSON 就缩进美化(截断内容解析失败则原样返回)
131
+ function prettyJson(s) {
132
+ if (typeof s !== 'string') return s;
133
+ try { const v = JSON.parse(s); return JSON.stringify(v, null, 2); } catch { return s; }
134
+ }
135
+
136
+ // 详情入库前的预处理:把昂贵的派生渲染(Markdown 正则渲染、每个工具的 JSON 美化)
137
+ // 一次性算好缓存在对象上(下划线字段)。之前这些都是模板里的函数调用 —— 卡片每次重渲染
138
+ // (点开折叠、来一条 SSE、别的卡片展开)都会全量重算,362 个工具的大卡片实测主线程 100~500ms。
139
+ // 模板侧一律读缓存、以 `|| prettyJson(...)` 兜底:缓存缺失时退回现算,行为与旧版一致。
140
+ function prepDetail(d) {
141
+ if (!d || d._prepped) return d;
142
+ if (d.assistant) d._assistantMd = renderMd(d.assistant);
143
+ for (const t of d.tools || []) {
144
+ if (t.input) t._inPretty = prettyJson(t.input);
145
+ if (t.output) t._outPretty = prettyJson(t.output);
146
+ }
147
+ d._prepped = true;
148
+ return d;
149
+ }
150
+ // 详情内容指纹(不含下划线缓存字段):reloadDetail 用来判断「这次拉回来的和手里的是不是同一份」
151
+ const detailSig = d => d ? JSON.stringify([d.user, d.assistant, d.spans, d.calls, d.tools, d.others, d.callsNote]) : '';
152
+
153
+ // 「LLM 调用明细」那行右侧的工具标记文案:按**名字去重计数**后再列。
154
+ // 同一次响应里并行调三个 Bash 是常事,原样铺出来就是「Bash · Bash · Bash」——
155
+ // 那不是信息,是噪声。收成「Bash×3」。
156
+ // limit 默认 2:一行里还要放模型、耗时、token,列全了会把它们挤没(完整的在 title 里)。
157
+ function toolText(list, limit) {
158
+ if (!list || !list.length) return '';
159
+ const order = [], cnt = new Map();
160
+ for (const n of list) { if (!cnt.has(n)) { cnt.set(n, 0); order.push(n); } cnt.set(n, cnt.get(n) + 1); }
161
+ const lim = limit || 2;
162
+ const head = order.slice(0, lim).map(n => (cnt.get(n) > 1 ? n + '×' + cnt.get(n) : n)).join(' · ');
163
+ return order.length > lim ? head + ' +' + (order.length - lim) : head;
164
+ }
165
+
166
+ async function copyRaw(ev, text) {
167
+ ev.stopPropagation();
168
+ const t = String(text ?? '');
169
+ try { await navigator.clipboard.writeText(t); }
170
+ catch {
171
+ // file:// 或旧浏览器无 clipboard 权限 → 走 execCommand 兜底
172
+ const ta = document.createElement('textarea');
173
+ ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0';
174
+ document.body.appendChild(ta); ta.select();
175
+ try { document.execCommand('copy'); } catch {}
176
+ ta.remove();
177
+ }
178
+ ElementPlus.ElMessage.success('已复制原文');
179
+ }
180
+
181
+ // 会话级的「相对时间」:列表里一眼看出哪个会话是刚跑过的
182
+ const fmtAgo = t => {
183
+ if (!t) return '—';
184
+ const d = Date.now() - t;
185
+ if (d < 60000) return '刚刚';
186
+ if (d < 3600000) return Math.floor(d / 60000) + ' 分钟前';
187
+ if (d < 86400000) return Math.floor(d / 3600000) + ' 小时前';
188
+ if (d < 86400000 * 30) return Math.floor(d / 86400000) + ' 天前';
189
+ return new Date(t).toLocaleDateString('zh-CN');
190
+ };
191
+ // 合计 token 一律现算:/api/session 与它的 entries[] 只给 tin/tout/tcache,**没有** total 字段
192
+ // (服务端 sessionsAgg 是在返回前现算 total 的,这里跟它同一口径)。
193
+ const sumTok = o => ((o && o.tin) || 0) + ((o && o.tout) || 0) + ((o && o.tcache) || 0);
194
+
195
+ // 聚合量(按天/整个范围)用的耗时格式:拿上面那个格式会算出「30089m41s」——200 天合计 501 小时,
196
+ // 分针读法到这儿就不可读了。单条卡片仍用 fmtDur(那里都是分钟级)。
197
+ const fmtDurBig = ms => {
198
+ if (!ms) return '—';
199
+ const h = ms / 3600000;
200
+ if (h >= 48) return (h / 24).toFixed(1) + '天';
201
+ if (h >= 1) return h.toFixed(1) + 'h';
202
+ return fmtDur(ms);
203
+ };
204
+ const fmtTime = t => t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—';
205
+
206
+ // 模型名一律先落成字符串再拼。原因:卡片标题就是 models.join(', '),而 join 对非字符串一律调
207
+ // toString —— 只要数组里混进一个对象(某个 agent 的某个版本把 model 写成 {slug:…}),标题就会
208
+ // 变成「[object Object], [object Object]…」:日志本身没坏,看起来却像整张卡片乱了。
209
+ // 服务端已经统一强制过(见 agent-acta-server.mjs 的 modelName / modelList);这里是页面侧第二道,
210
+ // 因为页面比服务端好更新——老服务端配新页面时也得能正常显示。
211
+ const modelText = list => (list || []).map(m => {
212
+ if (typeof m === 'string') return m.trim();
213
+ if (m && typeof m === 'object') return String(m.slug || m.id || m.model_id || m.name || '').trim();
214
+ return m == null ? '' : String(m);
215
+ }).filter(Boolean).join(', ');
216
+
217
+ // R20:工具名摘要收进「N 工具」旁的问号悬浮层(完整列表,不再截 5 个),详情仍展开完整工具列表。
218
+ const toolList = e => Object.entries(e.toolNames || {})
219
+ .filter(([n, c]) => n && c > 0)
220
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
221
+ const agentLetter = a => ({ atomcode: 'A', codebuddy: 'C', workbuddy: 'W' })[a] || (String(a || '?')[0] || '?').toUpperCase();
222
+ // 上下文进度条的分子是「这一轮结束时的上下文占用」,**不是 Token 合计**——
223
+ // 合计(e.total)是轮内各次调用的累加,跑 80 轮能到千万级,拿它除以窗口会永远贴着 100%。
224
+ // atomcode 有真实末轮占用(e.ctxUsed = meta.total_tokens = 末轮 prompt+completion);
225
+ // 其余 agent 只有累加值可近似,退回 e.total(原来就是这个行为,别改)。
226
+ const ctxUsed = e => e.ctxUsed != null ? e.ctxUsed : e.total;
227
+ const ctxPct = e => e.ctx ? Math.min(100, Math.round(ctxUsed(e) / e.ctx * 100)) : 0;
228
+
229
+ const agentColor = name => {
230
+ if (AGENT_COLORS[name]) return AGENT_COLORS[name];
231
+ let h = 0; for (const c of String(name)) h = (h * 31 + c.charCodeAt(0)) >>> 0;
232
+ return PALETTE[h % PALETTE.length];
233
+ };
234
+ const agentIcon = name => AGENT_ICONS[name] ? '/vendor/svg/' + AGENT_ICONS[name] : null;
235
+
236
+ // 前缀匹配表:模型名以这些前缀开头即命中(hy3/hy-t1→混元、seedance→豆包等),优先于子串匹配
237
+ const MODEL_PREFIX_ICONS = [
238
+ ['deepseek', 'deepseek-color.svg'],
239
+ ['qwen', 'qwen-color.svg'],
240
+ // 智谱系模型名实际都是 glm/GLM 开头(glm-4.7、GLM-5.3,见 zcode/trae/opencode 解析器 fixture),
241
+ // 一律走 zhipu-color.svg;glmv(GLM-V 视觉系)排在 glm 前防被吞
242
+ ['glmv', 'glmv-color.svg'],
243
+ ['glm', 'zhipu-color.svg'],
244
+ ['llama', 'meta-color.svg'],
245
+ ['openrouter', 'openrouter-color.svg'],
246
+ ['doubao', 'doubao-color.svg'], ['seed', 'doubao-color.svg'], // seedance/seed1.6 等字节系→豆包
247
+ ['hy', 'hunyuan-color.svg'], ['hunyuan', 'hunyuan-color.svg'],
248
+ ['kimi', 'kimi.svg'], ['moonshot', 'moonshot.svg'],
249
+ ['gemini', 'gemini-color.svg'],
250
+ ['grok', 'grok.svg'],
251
+ ['claude', 'claude-color.svg'],
252
+ ['gpt', 'openai.svg'], ['o3', 'openai.svg'], ['o4', 'openai.svg'],
253
+ ['step', 'stepfun.svg'], // step 系模型(step-xxx / stepfun)
254
+ ['minimax', 'minimax-color.svg'], ['abab', 'minimax-color.svg'],
255
+ ['ernie', 'wenxin-color.svg'], ['wenxin', 'wenxin-color.svg'],
256
+ ['mimo', 'xiaomimimo.svg'],
257
+ ['pangu', 'huawei-color.svg'],
258
+ ['yuanbao', 'yuanbao-color.svg'],
259
+ ['mai-code', 'copilot.svg'], // 蚂蚁 mai-code 系模型(mai-code-xxx)借 GitHub Copilot 图标
260
+ ['muse', 'meta-color.svg'], // Muse 系模型(muse-xxx)
261
+ ['nemotron', 'nvidia-color.svg'], // Nemotron 系模型(nemotron-xxx)
262
+ ['ling-', 'antgroup-color.svg'], // 蚂蚁百灵 Ling 系模型(ling-xxx)
263
+ ['sensenova', 'sensenova-color.svg'], ['sense-', 'sensenova-color.svg'], // 商汤(sensenova-u1 / sense-nova-xxx 两种写法)
264
+ ['nex-', 'nex-color.svg'], // Nex 系模型(Nex-N2.5-Pro/Mini/Max)
265
+ ];
266
+ const modelIcon = models => {
267
+ const list = models || [];
268
+ for (const m of list) {
269
+ const s = String(m).toLowerCase();
270
+ for (const [p, f] of MODEL_PREFIX_ICONS) if (s.startsWith(p)) return '/vendor/svg/' + f;
271
+ }
272
+ const joined = modelText(list).toLowerCase(); // 非字符串一律先落成字符串,否则 {slug:…} 会变成 "[object object]" 匹配不上任何前缀
273
+ for (const [k, f] of MODEL_ICONS) if (joined.includes(k)) return '/vendor/svg/' + f;
274
+ return null;
275
+ };
276
+ const iconOf = e => modelIcon(e.models) || agentIcon(e.agent);
277
+
278
+ // ---- R11 导出纯函数(CSV / Excel)----
279
+ // 这里只放**不依赖响应式状态**的序列化与下载触发,筛选条件怎么拼由根 setup 决定(它才认识那些 ref)。
280
+ // 约束同 shared.js 头部:不引外部库(离线本地服务,CDN 不可用)、零依赖、手写格式。
281
+ // CSV 与 Excel(SpreadsheetML) 都带 UTF-8 BOM —— 中文在表格软件里不乱码。
282
+
283
+ // CSV 单元格转义:值含逗号/双引号/换行时用双引号包起来,内部双引号翻倍。
284
+ function csvCell(v) {
285
+ const s = String(v ?? '');
286
+ return /[",\r\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
287
+ }
288
+ // 组装 CSV:lines 已含表头与筛选说明行。
289
+ function buildCSV(lines) {
290
+ return '\uFEFF' + lines.map(row => row.map(csvCell).join(',')).join('\r\n');
291
+ }
292
+ // 触发浏览器下载。
293
+ function downloadBlob(blob, filename) {
294
+ const a = document.createElement('a');
295
+ a.href = URL.createObjectURL(blob);
296
+ a.download = filename;
297
+ a.click();
298
+ URL.revokeObjectURL(a.href);
299
+ }
300
+ // Excel 兼容文件:SpreadsheetML 2003 XML(.xls)。Excel / WPS 原生打开,无需任何第三方库。
301
+ // sheetName 当工作表名;说明行(noteLines)+ 空行 + 数据表头 + 数据行,满足「能自证按什么条件导出」。
302
+ function buildExcelXLSText(sheetName, noteLines, header, rows) {
303
+ const ssCell = v => {
304
+ const s = String(v ?? '');
305
+ const esc = s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
306
+ const isNum = /^-?\d+(\.\d+)?$/.test(s);
307
+ return '<Cell><Data ss:Type="' + (isNum ? 'Number' : 'String') + '">' + esc + '</Data></Cell>';
308
+ };
309
+ const rowXml = cells => '<Row>' + cells.map(ssCell).join('') + '</Row>';
310
+ // 说明行横跨到第 2 列(表头列数未知前先固定用长度 2):值放第 1 列即可,第 2 列留空当分隔。
311
+ const noteRows = (noteLines || []).map(t => '<Row><Cell><Data ss:Type="String">' + String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</Data></Cell><Cell></Cell></Row>');
312
+ const dataRows = rows.map(r => rowXml(header.map(h => r[h])));
313
+ const xml =
314
+ '<?xml version="1.0"?>\n' +
315
+ '<?mso-application progid="Excel.Sheet"?>\n' +
316
+ '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"\n' +
317
+ ' xmlns:o="urn:schemas-microsoft-com:office:office"\n' +
318
+ ' xmlns:x="urn:schemas-microsoft-com:office:excel"\n' +
319
+ ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">\n' +
320
+ ' <Worksheet ss:Name="' + String(sheetName).replace(/[\\*?[\]]/g, '_') + '">\n' +
321
+ ' <Table>\n' +
322
+ noteRows.join('\n') +
323
+ '<Row></Row>\n' +
324
+ rowXml(header) + '\n' +
325
+ dataRows.join('\n') + '\n' +
326
+ ' </Table>\n' +
327
+ ' </Worksheet>\n' +
328
+ '</Workbook>';
329
+ return '\uFEFF' + xml;
330
+ }
331
+
332
+ // ---------------- I8 单轮「复现包」:零依赖 ZIP 打包 ----------------
333
+ // 约束:本地离线服务、浏览器里也禁第三方库,所以**不能**引 JSZip 之类。这里手写 store 模式(不压缩)
334
+ // 的 ZIP,跨平台、各类解压工具与 `unzip` 都认。内容是文本为主,store 不压缩并无所谓。
335
+ // files: [{ name, text }],name 用正斜杠(`/`)当分隔符、末尾不带 `/`;text 任意字符串(按 UTF-8 落盘)。
336
+ // 返回 Blob(type='application/zip'),直接丢给 downloadBlob。
337
+ const _crcTable = (() => {
338
+ const t = new Uint32Array(256);
339
+ for (let n = 0; n < 256; n++) {
340
+ let c = n;
341
+ for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
342
+ t[n] = c >>> 0;
343
+ }
344
+ return t;
345
+ })();
346
+ function _crc32(bytes) {
347
+ let c = 0xFFFFFFFF;
348
+ for (let i = 0; i < bytes.length; i++) c = _crcTable[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8);
349
+ return (c ^ 0xFFFFFFFF) >>> 0;
350
+ }
351
+ function makeZip(files) {
352
+ const enc = new TextEncoder();
353
+ const chunks = []; // 所有写入的 Uint8Array 片段(按 ZIP 流顺序)
354
+ const central = []; // 中央目录记录(每个文件一条)
355
+ let offset = 0; // 当前本地文件头起点(相对 ZIP 起点)
356
+ const u16 = n => { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; };
357
+ const u32 = n => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n >>> 0, true); return b; };
358
+ for (const f of files) {
359
+ const nameBytes = enc.encode(f.name);
360
+ const data = enc.encode(String(f.text ?? ''));
361
+ const crc = _crc32(data);
362
+ // 本地文件头(30 字节签名 + name)
363
+ const local = [
364
+ u32(0x04034b50), // local file header signature
365
+ u16(20), // version needed
366
+ u16(0x0800), // general purpose flag: bit 11 = UTF-8 文件名(中文名不乱码)
367
+ u16(0), // compression method: 0 = store
368
+ u16(0), u16(0), // mod time / mod date(留 0,复现包不在乎)
369
+ u32(crc), // CRC-32
370
+ u32(data.length), // compressed size(store = 原长)
371
+ u32(data.length), // uncompressed size
372
+ u16(nameBytes.length), u16(0), // file name / extra length
373
+ nameBytes,
374
+ ];
375
+ chunks.push(...local, data);
376
+ // 中央目录记录
377
+ const cd = [
378
+ u32(0x02014b50), // central directory header signature
379
+ u16(20), u16(20), // version made by / needed
380
+ u16(0x0800), // flag: UTF-8
381
+ u16(0), // method: store
382
+ u16(0), u16(0), // time / date
383
+ u32(crc),
384
+ u32(data.length),
385
+ u32(data.length),
386
+ u16(nameBytes.length), u16(0), u16(0), // name / extra / comment length
387
+ u32(0), // disk number start
388
+ u16(0), // internal attrs
389
+ u32(0), // external attrs
390
+ u32(offset), // local header offset
391
+ nameBytes,
392
+ ];
393
+ central.push({ bytes: cd, len: cd.reduce((a, b) => a + b.length, 0) });
394
+ offset += local.reduce((a, b) => a + b.length, 0) + data.length;
395
+ }
396
+ const cdStart = offset;
397
+ let cdSize = 0;
398
+ for (const c of central) { chunks.push(...c.bytes); cdSize += c.len; }
399
+ // 结束目录记录(End of Central Directory)
400
+ const eocd = [
401
+ u32(0x06054b50), u16(0), u16(0), // signature + disk numbers
402
+ u16(files.length), u16(files.length), // entries on this / total
403
+ u32(cdSize), u32(cdStart), // central dir size / offset
404
+ u16(0), // comment length
405
+ ];
406
+ chunks.push(...eocd);
407
+ return new Blob(chunks, { type: 'application/zip' });
408
+ }
@@ -0,0 +1,129 @@
1
+ // 侧栏(R25 批 2):菜单(两个视图 + 活跃热力 / 历史归档 / 环境诊断 / 磁盘占用 / 解析器自检 / 上下文窗口六个弹层)
2
+ // + Agent 列表(Trae 解密三态 / Cursor token 采集 hook / 启停开关 / 移除)+ 非 LLM 轨迹勾选。
3
+ // 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
4
+ //
5
+ // 数据进出(搬的时候逐个对过):
6
+ // · 纯展示 + 发命令:view / fAgent / agents / agentsMeta / sideAgents / showNoLLM 全是根的状态(筛选五元组
7
+ // 喂 snapUrl、深链在根),一份真相留根;连 isDisabled / isMissing / pathTip / isTraeKeyAgent 这几个判定
8
+ // 也留在根,因为它们闭包读 agentsMeta —— 搬过来就是第二份 agentsMeta。
9
+ // · agentColor / agentIcon 是 page/shared.js 的顶层函数,本文件的 JS 作用域看得见,
10
+ // 但**模板**看不见页面脚本的顶层名,所以 setup 里 return 出去(与 dialog-stats -return-fmtN 同法)。
11
+ // · 一处接线纠正(2026-09-21 批 2):改前侧栏写的是 @command="traeCommandUI",而 el-dropdown 的 @command
12
+ // 第二个实参是 EP 自己传进来的组件实例(实测 vendor bundle:commandHandler(command, instance, event)),
13
+ // 不是 v-for 的 agent 名 —— 于是 traeCommandUI 里的 `if (!name) return` 永不触发、traeBusy 被写成实例对象,
14
+ // 「这一行的按钮转圈 / 禁用」一直没生效。组件这边由 v-for 的 name 显式随命令传出,行为回到注释描述的三态。
15
+ window.AACTA = window.AACTA || {};
16
+ AACTA.Sidebar = {
17
+ name: 'aa-sidebar',
18
+ template: `
19
+ <aside class="side">
20
+ <div class="nav-label" style="padding-top:2px">菜单</div>
21
+ <!-- R6:这里变成两个真实视图('log' | 'sess'),所以第一项不再是硬编码的 active。
22
+ 「环境诊断」「上下文窗口」保持原样 —— 它们是弹层,不是视图。 -->
23
+ <div class="nav-item" :class="{active: view==='log'}" @click="$emit('switch-view', 'log')">
24
+ <el-icon><Document></Document></el-icon>请求日志
25
+ </div>
26
+ <div class="nav-item" :class="{active: view==='sess'}" @click="$emit('switch-view', 'sess')">
27
+ <el-icon><Files></Files></el-icon>按会话浏览
28
+ </div>
29
+ <!-- R27:活跃热力图弹层(数据固定 range=all、不跟随筛选,与「环境诊断」一样是独立弹层) -->
30
+ <div class="nav-item" @click="$emit('open-heatmap')">
31
+ <el-icon><Calendar></Calendar></el-icon>活跃热力
32
+ </div>
33
+ <!-- R28:历史归档弹层(读 ~/.agent-acta/archive 下的冻结条目,只读旁路、不跟随筛选) -->
34
+ <div class="nav-item" @click="$emit('open-archive')">
35
+ <el-icon><Box></Box></el-icon>历史归档
36
+ </div>
37
+ <!-- R9:异常巡检弹层(规则配置 + 命中条目列表)。徽章 = 当前命中的异常条数;
38
+ 有命中时高亮(红色),无命中一个都不报警,规则没启用也自然为 0 -->
39
+ <div class="nav-item" @click="$emit('open-alert')">
40
+ <el-icon><Bell></Bell></el-icon>异常巡检
41
+ <span v-if="alertCount" class="cnt alert-cnt" :title="alertCount + ' 条异常命中,点开可查看并定位'">{{ alertCount }}</span>
42
+ </div>
43
+ <div class="nav-item" @click="$emit('open-diag')">
44
+ <el-icon><Monitor></Monitor></el-icon>环境诊断
45
+ </div>
46
+ <!-- R30:磁盘占用弹层(走一遍磁盘的只读统计,与筛选无关,所以也是独立弹层) -->
47
+ <div class="nav-item" @click="$emit('open-usage')">
48
+ <el-icon><Coin></Coin></el-icon>磁盘占用
49
+ </div>
50
+ <!-- R32:解析器自检弹层(只读展示上次回归结果;跑测试留在终端,见 page/dialog-selftest.js 头部) -->
51
+ <div class="nav-item" @click="$emit('open-selftest')">
52
+ <el-icon><Checked></Checked></el-icon>解析器自检
53
+ </div>
54
+ <div class="nav-item" @click="$emit('open-ctxw')">
55
+ <el-icon><data-line></data-line></el-icon>上下文窗口
56
+ <!-- 徽章 = 日志里出现过、这张表里却查不到窗口的模型数。没有它,「少了点什么」就只是个感觉:
57
+ 得点进去才知道是 2 个模型没填还是压根没有 claude 日志 -->
58
+ <span v-if="ctxUnfilled" class="cnt ctxw-cnt" :title="ctxUnfilled + ' 个模型没有窗口值,这些卡片只显示占用、不画进度条'">{{ ctxUnfilled }}</span>
59
+ </div>
60
+ <div class="nav-label agent-label">Agent
61
+ <el-icon class="add-btn" title="添加 agent" @click="$emit('open-add')"><Plus></Plus></el-icon>
62
+ </div>
63
+ <div class="nav-item" :class="{active: fAgent===''}" @click="$emit('pick-agent', '')">
64
+ <span class="adot" style="background:#a8abb2"></span><span class="agent-name">全部</span><span class="cnt">{{ allCount }}</span>
65
+ </div>
66
+ <div v-for="name in sideAgents" :key="name" class="nav-item" :class="{active: fAgent===name, off: isDisabled(name) || isIconOnly(name)}" @click="$emit('pick-agent', name)" :title="pathTip(name)">
67
+ <img v-if="agentIcon(name)" class="adot-img" :class="{dim: isIconOnly(name)}" :src="agentIcon(name)">
68
+ <span v-else class="adot" :style="{background: agentColor(name)}"></span>
69
+ <span class="agent-name">{{ name }}</span>
70
+ <span v-if="isIconOnly(name)" class="icon-only-tag" title="该 agent 只有图标:暂无解析器,不会自动发现,点击无效。若已安装且想接入,请在 GitHub 提 issue 提供日志样本">仅图标</span>
71
+ <el-icon v-if="isMissing(name)" class="mini-warn" title="日志目录不存在,条目来自内存或手动配置"><Warning></Warning></el-icon>
72
+ <span class="cnt">{{ agents[name] || 0 }}</span>
73
+ <!-- Trae 的解密按钮(三态):无密钥→「抓取解密密钥」/ 有密钥但读不动→「重抓密钥」/ 正常→菜单(重新抓取/清除密钥) -->
74
+ <el-dropdown v-if="isTraeKeyAgent(name)" trigger="click" placement="bottom-end" @command="cmd => $emit('trae-command', cmd, name)" :disabled="traeBusy===name" :hide-on-click="false">
75
+ <el-icon class="dec-btn" :class="{busy: traeBusy===name}" title="解密 Trae 的 SQLCipher 库:没 key 就抓取、有 key 但读不动就重抓、都正常就显示菜单">
76
+ <Key></Key>
77
+ </el-icon>
78
+ <template #dropdown>
79
+ <el-dropdown-menu>
80
+ <el-dropdown-item command="trae-retry" :disabled="traeBusy===name">重新抓取密钥</el-dropdown-item>
81
+ <el-dropdown-item command="trae-clear" divided>清除密钥</el-dropdown-item>
82
+ </el-dropdown-menu>
83
+ </template>
84
+ </el-dropdown>
85
+ <!-- Cursor 的 token 采集 hook 按钮:Cursor 的转录/会话库都不含逐轮用量,只能靠它的 stop 钩子采
86
+ (见 parsers/cursor.mjs 头部说明)。状态由服务端给(agentsMeta[name].hook),页面只读:
87
+ 高亮 = 已注入;高亮 + 转圈 = 正在注入/卸载;灰着 = 还没注入。点一下走根里的 toggleCursorHook。 -->
88
+ <el-icon v-if="isCursorHookAgent(name)" class="hook-btn" :class="{on: cursorHookState(name).injected, busy: cursorHookBusy===name}"
89
+ :title="cursorHookState(name).injected ? '已注入 token 采集 hook(每轮结束时采集用量)。点一下可卸载'
90
+ : '注入 token 采集 hook:Cursor 转录里没有逐轮用量,装上后每轮结束时把 token 记到本地'"
91
+ @click.stop="$emit('cursor-hook', name)">
92
+ <MagicStick></MagicStick>
93
+ </el-icon>
94
+ <el-switch class="sw" :model-value="!isDisabled(name)" :disabled="toggleLoading===name"
95
+ title="启用 / 禁用(禁用不移除配置,仅停止扫描并隐藏条目)" @click.stop @change="v => $emit('toggle-agent', name, v)"></el-switch>
96
+ <el-icon v-if="agentsMeta[name]" class="del-btn" title="移除该 agent(只删配置,不动本地日志文件)" @click.stop="$emit('remove-agent', name)"><Close></Close></el-icon>
97
+ </div>
98
+ <div class="side-foot">
99
+ <el-checkbox :model-value="noLlm" @update:model-value="v => $emit('update:no-llm', v)">显示非 LLM 轨迹</el-checkbox>
100
+ </div>
101
+ </aside>`,
102
+ props: {
103
+ view: { type: String, default: 'log' },
104
+ ctxUnfilled: { type: Number, default: 0 },
105
+ alertCount: { type: Number, default: 0 },
106
+ fAgent: { type: String, default: '' },
107
+ allCount: { type: Number, default: 0 },
108
+ sideAgents: { type: Array, default: () => [] },
109
+ agents: { type: Object, default: () => ({}) },
110
+ agentsMeta: { type: Object, default: () => ({}) },
111
+ noLlm: Boolean,
112
+ traeBusy: { type: String, default: '' },
113
+ toggleLoading: { type: String, default: '' },
114
+ // 四个判定函数留在根(读根的 agentsMeta),连同按钮的三态状态一起递进来
115
+ isDisabled: { type: Function, required: true },
116
+ isIconOnly: { type: Function, required: true },
117
+ isMissing: { type: Function, required: true },
118
+ pathTip: { type: Function, required: true },
119
+ isTraeKeyAgent: { type: Function, required: true },
120
+ // Cursor token 采集 hook:判定与状态都是函数(闭包读根的 agentsMeta,搬过来就是第二份)
121
+ cursorHookBusy: { type: String, default: '' },
122
+ isCursorHookAgent: { type: Function, required: true },
123
+ cursorHookState: { type: Function, required: true },
124
+ },
125
+ emits: ['switch-view', 'open-diag', 'open-usage', 'open-selftest', 'open-ctxw', 'open-heatmap', 'open-archive', 'open-alert', 'open-add', 'pick-agent', 'trae-command', 'cursor-hook', 'toggle-agent', 'remove-agent', 'update:no-llm'],
126
+ setup() {
127
+ return { agentColor, agentIcon };
128
+ }
129
+ };