@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.
- package/LICENSE +21 -0
- package/LOGFORMATS.md +960 -0
- package/README.md +238 -0
- package/REFERENCE.md +1428 -0
- package/agent-acta-page.html +1998 -0
- package/agent-acta-server.mjs +1066 -0
- package/agent-acta-widget.html +724 -0
- package/archive.mjs +335 -0
- package/core/log.mjs +20 -0
- package/core/service.mjs +4285 -0
- package/hooks/cursor-usage-hook.mjs +89 -0
- package/mcp-server.mjs +372 -0
- package/package.json +68 -0
- package/page/dialog-add-agent.js +65 -0
- package/page/dialog-alert.js +159 -0
- package/page/dialog-archive.js +342 -0
- package/page/dialog-compare.js +238 -0
- package/page/dialog-ctxw.js +101 -0
- package/page/dialog-diag.js +72 -0
- package/page/dialog-heatmap.js +215 -0
- package/page/dialog-selftest.js +202 -0
- package/page/dialog-sess-timeline.js +108 -0
- package/page/dialog-stats.js +694 -0
- package/page/dialog-usage.js +158 -0
- package/page/log-card.js +277 -0
- package/page/log-view.js +273 -0
- package/page/sess-view.js +382 -0
- package/page/shared.js +408 -0
- package/page/sidebar.js +129 -0
- package/page/style.css +973 -0
- package/page/topbar.js +61 -0
- package/parsers/atomcode.mjs +321 -0
- package/parsers/buddy.mjs +272 -0
- package/parsers/buddyext.mjs +582 -0
- package/parsers/claude.mjs +496 -0
- package/parsers/cline.mjs +468 -0
- package/parsers/codearts.mjs +96 -0
- package/parsers/codex.mjs +188 -0
- package/parsers/comate.mjs +195 -0
- package/parsers/copilot.mjs +308 -0
- package/parsers/cursor.mjs +493 -0
- package/parsers/devindb.mjs +358 -0
- package/parsers/discovery.mjs +1230 -0
- package/parsers/doubao.mjs +406 -0
- package/parsers/dsh.mjs +377 -0
- package/parsers/gemini.mjs +267 -0
- package/parsers/generic.mjs +494 -0
- package/parsers/hermes.mjs +398 -0
- package/parsers/kilo.mjs +42 -0
- package/parsers/kimi.mjs +249 -0
- package/parsers/mimocode.mjs +531 -0
- package/parsers/minimax.mjs +503 -0
- package/parsers/openclaw.mjs +490 -0
- package/parsers/opencode.mjs +465 -0
- package/parsers/shared.mjs +418 -0
- package/parsers/trace.mjs +66 -0
- package/parsers/tracecode.mjs +348 -0
- package/parsers/traedb.mjs +528 -0
- package/parsers/zcode.mjs +356 -0
- package/plugin/client.js +51 -0
- package/plugin/cordis.patch.yml +5 -0
- package/plugin/plugin.mjs +220 -0
- package/search.mjs +554 -0
- package/tools/grab-trae-key.ps1 +265 -0
- package/vendor/element-plus-icons.js +3 -0
- package/vendor/element-plus.css +1 -0
- package/vendor/element-plus.dark.css +1 -0
- package/vendor/element-plus.full.min.js +65 -0
- package/vendor/element-plus.zh-cn.js +2 -0
- package/vendor/svg/antgroup-color.svg +1 -0
- package/vendor/svg/bytedance-color.svg +1 -0
- package/vendor/svg/chatglm-color.svg +1 -0
- package/vendor/svg/claude-color.svg +1 -0
- package/vendor/svg/claudecode-color.svg +1 -0
- package/vendor/svg/cline.svg +1 -0
- package/vendor/svg/codebuddy-color.svg +1 -0
- package/vendor/svg/codebuddy.svg +1 -0
- package/vendor/svg/codex-color.svg +1 -0
- package/vendor/svg/copilot.svg +1 -0
- package/vendor/svg/cursor.svg +1 -0
- package/vendor/svg/deepseek-color.svg +1 -0
- package/vendor/svg/deepseek-harness-whale.svg +8 -0
- package/vendor/svg/doubao-color.svg +1 -0
- package/vendor/svg/gemini-color.svg +1 -0
- package/vendor/svg/glmv-color.svg +1 -0
- package/vendor/svg/grok.svg +1 -0
- package/vendor/svg/hermesagent.svg +1 -0
- package/vendor/svg/huawei-color.svg +1 -0
- package/vendor/svg/hunyuan-color.svg +1 -0
- package/vendor/svg/kilocode-avatar.svg +1 -0
- package/vendor/svg/kilocode.svg +1 -0
- package/vendor/svg/kimi-color.svg +1 -0
- package/vendor/svg/kimi.svg +1 -0
- package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
- package/vendor/svg/logo-AtomGit-dark.svg +4 -0
- package/vendor/svg/logo-AtomGit-light.svg +4 -0
- package/vendor/svg/meta-color.svg +1 -0
- package/vendor/svg/mimo.svg +66 -0
- package/vendor/svg/minimax-code-logo.svg +8 -0
- package/vendor/svg/minimax-color.svg +1 -0
- package/vendor/svg/moonshot.svg +1 -0
- package/vendor/svg/nex-color.svg +1 -0
- package/vendor/svg/nex.svg +1 -0
- package/vendor/svg/nvidia-color.svg +1 -0
- package/vendor/svg/openai.svg +1 -0
- package/vendor/svg/openclaw-color.svg +1 -0
- package/vendor/svg/openclaw.svg +1 -0
- package/vendor/svg/opencode.svg +1 -0
- package/vendor/svg/openrouter-color.svg +1 -0
- package/vendor/svg/qoder-color.svg +1 -0
- package/vendor/svg/qwen-color.svg +1 -0
- package/vendor/svg/sensenova-color.svg +1 -0
- package/vendor/svg/stepfun.svg +1 -0
- package/vendor/svg/trae-color.svg +1 -0
- package/vendor/svg/traework-v2.svg +1 -0
- package/vendor/svg/wenxin-color.svg +1 -0
- package/vendor/svg/windsurf.svg +1 -0
- package/vendor/svg/workbuddy-color.svg +29 -0
- package/vendor/svg/xiaomimimo.svg +1 -0
- package/vendor/svg/yuanbao-color.svg +1 -0
- package/vendor/svg/yuanbao.svg +1 -0
- package/vendor/svg/zai.svg +1 -0
- package/vendor/svg/zhipu-color.svg +1 -0
- package/vendor/vue.global.prod.js +9 -0
- package/widget/main.js +202 -0
- package/widget/package.json +7 -0
- package/widget/preload.js +11 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// 磁盘占用弹层(R30 / 候选池 I3):一眼看清「哪个 agent 的日志最占地方、清理时该动谁」。
|
|
2
|
+
// 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
|
|
3
|
+
//
|
|
4
|
+
// 数据进出(与 dialog-diag / dialog-archive 同法:开关归根、数据归组件):
|
|
5
|
+
// · visible ← 根(侧栏「磁盘占用」项点开 / 弹层关掉时回写)
|
|
6
|
+
// · getJSON ← 根 provide(接口 404 要点亮顶部版本红条,那段逻辑只有根里一份)
|
|
7
|
+
// · 自己拉 /api/usage(服务端一次遍历三处:源日志目录 / index 分片 / archive 清单)
|
|
8
|
+
//
|
|
9
|
+
// 四处口径要留意(都是服务端刻意这么定的,页面只是别把它藏起来):
|
|
10
|
+
// · 索引**单列**、不并进 agent 行:一个 kind 的分片可能同时供好几个 agent,相加就是重复计数。
|
|
11
|
+
// · partial(统计到一半)必须显式说明:遍历带预算,宁可报「这是部分」也不给假的完整数字。
|
|
12
|
+
// · 归档条数 -1 = manifest 没记(手工拷进来的日期文件),显示成 ?,不是 0。
|
|
13
|
+
// · 两个 agent 指同一份目录时:行内各报各的,**合计去重**(totals.logBytes 与 logBytesRows 不等即为共用)。
|
|
14
|
+
window.AACTA = window.AACTA || {};
|
|
15
|
+
AACTA.DialogUsage = {
|
|
16
|
+
name: 'aa-dialog-usage',
|
|
17
|
+
template: `
|
|
18
|
+
<el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="磁盘占用" width="min(1000px, 95vw)" append-to-body class="us-dialog">
|
|
19
|
+
<div class="us-hint">
|
|
20
|
+
下面按 agent 列出<b>本机磁盘上真实占了多少</b>:日志 = 它自己的日志目录(库类连 -wal/-shm 一起算),
|
|
21
|
+
归档 = <code>~/.agent-acta/archive</code> 里冻结的历史。这一页<b>只统计、不删除</b>,
|
|
22
|
+
要清理请照着自己判断的路径动手(先看一眼 <code>agentacta --doctor</code> 更稳)。
|
|
23
|
+
</div>
|
|
24
|
+
<el-skeleton v-if="us.loading && !us.rows.length" :rows="6" animated></el-skeleton>
|
|
25
|
+
<div v-else-if="us.error" class="us-err">
|
|
26
|
+
<div class="us-errmsg">{{ us.error }}</div>
|
|
27
|
+
<el-button size="small" :loading="us.loading" @click="run(true)">重试</el-button>
|
|
28
|
+
</div>
|
|
29
|
+
<div v-else>
|
|
30
|
+
<div class="us-sum">
|
|
31
|
+
<span>日志 <b>{{ b(us.totals.logBytes) }}</b><template v-if="us.totals.logBytesRows !== us.totals.logBytes">(已去重共用目录)</template></span><i>·</i>
|
|
32
|
+
<span>归档 <b>{{ b(us.archive.bytes) }}</b></span><i>·</i>
|
|
33
|
+
<span>索引 <b>{{ b(us.totals.indexBytes) }}</b></span><i>·</i>
|
|
34
|
+
<span>数据目录 <b>{{ b(us.totals.dataBytes) }}</b></span>
|
|
35
|
+
<div class="us-sum-2">
|
|
36
|
+
{{ us.totals.agents }} 个 agent · {{ fmtN(us.totals.logFiles) }} 个日志文件 ·
|
|
37
|
+
归档 {{ us.archive.agents }} 个 agent / {{ us.archive.days }} 个日期文件 ·
|
|
38
|
+
统计于 {{ us.at ? fmtTime(us.at) : '—' }}<template v-if="us.cached">(用的一分钟内的结果)</template>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<!-- 超预算:数字是「已统计的部分」。不写这一行的话,一个偏小的总数会被当成事实。 -->
|
|
42
|
+
<div v-if="us.partial" class="us-warn">
|
|
43
|
+
本次遍历到达上限(最多 {{ fmtN(us.budget.maxFiles) }} 个目录项 / {{ us.budget.maxMs }}ms),
|
|
44
|
+
下面的数字是<b>已统计的部分</b>,比磁盘上真实的要小。
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="us-cap">按 agent</div>
|
|
48
|
+
<div class="us-head">
|
|
49
|
+
<span class="us-c nm" @click="sortBy('agent')" :class="{on: key==='agent'}">Agent <i>{{ arrow('agent') }}</i></span>
|
|
50
|
+
<span class="us-c num" @click="sortBy('logBytes')" :class="{on: key==='logBytes'}">日志 <i>{{ arrow('logBytes') }}</i></span>
|
|
51
|
+
<span class="us-c num" @click="sortBy('archiveBytes')" :class="{on: key==='archiveBytes'}">归档 <i>{{ arrow('archiveBytes') }}</i></span>
|
|
52
|
+
<span class="us-c num" @click="sortBy('entries')" :class="{on: key==='entries'}">内存条目 <i>{{ arrow('entries') }}</i></span>
|
|
53
|
+
<span class="us-c num" @click="sortBy('totalBytes')" :class="{on: key==='totalBytes'}">合计 <i>{{ arrow('totalBytes') }}</i></span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="us-list">
|
|
56
|
+
<div v-for="r in sortedRows" :key="r.agent" class="us-row" :class="{off: !r.enabled}">
|
|
57
|
+
<span class="us-c nm">
|
|
58
|
+
<img v-if="agentIcon(r.agent)" class="us-ico" :src="agentIcon(r.agent)">
|
|
59
|
+
<span v-else class="us-dot" :style="{background: agentColor(r.agent)}"></span>
|
|
60
|
+
<span class="us-nm">{{ r.agent }}</span>
|
|
61
|
+
<span class="us-kind">{{ r.kind }}</span>
|
|
62
|
+
<span v-if="!r.enabled" class="us-tag" title="已禁用:不再扫描,但磁盘上的日志仍然占着地方">已禁用</span>
|
|
63
|
+
<span v-if="r.sharedBy" class="us-tag" :title="'与 ' + r.sharedBy + ' 指向同一份目录:两边各报各的大小,合计只算一次'">共用目录</span>
|
|
64
|
+
<span v-if="r.partial" class="us-tag part" title="这个 agent 的目录太大,统计被预算截断">统计到一半</span>
|
|
65
|
+
</span>
|
|
66
|
+
<span class="us-c num" :title="r.roots.join('\\n')">
|
|
67
|
+
{{ b(r.logBytes) }}<i class="us-sub">{{ fmtN(r.logFiles) }} 文件</i>
|
|
68
|
+
</span>
|
|
69
|
+
<span class="us-c num">
|
|
70
|
+
{{ r.archiveBytes ? b(r.archiveBytes) : '—' }}
|
|
71
|
+
<i v-if="r.archiveDays" class="us-sub">{{ r.archiveDays }} 天 / {{ r.archiveCount < 0 ? '?' : fmtN(r.archiveCount) }} 条</i>
|
|
72
|
+
</span>
|
|
73
|
+
<span class="us-c num">{{ r.entries ? fmtN(r.entries) : '—' }}</span>
|
|
74
|
+
<span class="us-c num tot">
|
|
75
|
+
{{ b(r.totalBytes) }}
|
|
76
|
+
<!-- 条长 = 占最大那一行的比例(不是占总量的比例):这一列要比的是「谁比谁大多少」 -->
|
|
77
|
+
<span class="us-bar" :style="{width: barPct(r.totalBytes) + '%'}"></span>
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div v-if="!us.rows.length" class="us-none">还没有配置任何 agent。</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="us-cap">索引分片 <span class="us-cap-note">~/.agent-acta/index —— 一片可能同时供多个 agent,所以不并进上面的行</span></div>
|
|
84
|
+
<div class="us-shards">
|
|
85
|
+
<span v-for="s in us.indexShards" :key="s.kind" class="us-shard"
|
|
86
|
+
:class="{orphan: !s.persisted}" :title="s.persisted ? '解析器口径 rev ' + s.rev : '这个 kind 已不再落盘(孤儿片,可手工删)'">
|
|
87
|
+
{{ s.kind }} <b>{{ b(s.bytes) }}</b><i v-if="!s.persisted">孤儿</i>
|
|
88
|
+
</span>
|
|
89
|
+
<span v-if="!us.indexShards.length" class="us-none">没有索引文件。</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="us-cap">数据目录构成 <span class="us-cap-note">{{ us.dataDir }}</span></div>
|
|
93
|
+
<div class="us-shards">
|
|
94
|
+
<span v-for="d in us.dataItems" :key="d.name" class="us-shard" :class="{orphan: d.link}"
|
|
95
|
+
:title="d.link ? '链接项:不跟随、不计大小(跟过去要么重复计数、要么转圈)' : (d.dir ? '目录:' + fmtN(d.files) + ' 个文件' : '文件')">
|
|
96
|
+
{{ d.name }} <b>{{ d.link ? '—' : b(d.bytes) }}</b><i v-if="d.partial">到一半</i><i v-if="d.link">链接</i>
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<template #footer>
|
|
101
|
+
<span class="us-foot">「内存条目」是服务当前留着的条数(有上限,重启后会重新扫回来),与磁盘占用无关。
|
|
102
|
+
这一页只读:删任何东西都要你自己动手 —— 日志删了历史就没了(想留先开归档),索引片删了下次启动会重扫一遍。</span>
|
|
103
|
+
<el-button @click="$emit('update:visible', false)">关闭</el-button>
|
|
104
|
+
<el-button :loading="us.loading" @click="run(false)">刷新(用缓存)</el-button>
|
|
105
|
+
<el-button type="primary" :loading="us.loading" @click="run(true)">重新统计</el-button>
|
|
106
|
+
</template>
|
|
107
|
+
</el-dialog>`,
|
|
108
|
+
props: { visible: Boolean },
|
|
109
|
+
emits: ['update:visible'],
|
|
110
|
+
setup(props) {
|
|
111
|
+
const getJSON = Vue.inject('getJSON');
|
|
112
|
+
const us = Vue.reactive({
|
|
113
|
+
loading: false, error: '', at: 0, cached: false, partial: false, dataDir: '',
|
|
114
|
+
rows: [], indexShards: [], dataItems: [],
|
|
115
|
+
archive: { agents: 0, days: 0, count: 0, bytes: 0 },
|
|
116
|
+
totals: { logBytes: 0, archiveBytes: 0, indexBytes: 0, dataBytes: 0, agents: 0, logFiles: 0 },
|
|
117
|
+
budget: { maxFiles: 0, maxMs: 0, visited: 0 },
|
|
118
|
+
});
|
|
119
|
+
async function run(force) {
|
|
120
|
+
if (us.loading) return;
|
|
121
|
+
us.loading = true;
|
|
122
|
+
try {
|
|
123
|
+
const j = await getJSON('/api/usage' + (force ? '?force=1' : ''));
|
|
124
|
+
if (!j.ok) throw new Error(j.error || '统计失败');
|
|
125
|
+
us.at = j.at; us.cached = !!j.cached; us.partial = !!j.partial;
|
|
126
|
+
us.rows = j.agents || []; us.indexShards = j.indexShards || []; us.dataItems = j.dataItems || [];
|
|
127
|
+
us.archive = j.archive || us.archive; us.totals = j.totals || us.totals; us.budget = j.budget || us.budget;
|
|
128
|
+
us.dataDir = j.dataDir || '';
|
|
129
|
+
us.error = '';
|
|
130
|
+
} catch (e) { us.error = '磁盘占用统计失败:' + e.message; }
|
|
131
|
+
finally { us.loading = false; }
|
|
132
|
+
}
|
|
133
|
+
// 点开才统计(与「环境诊断」一样是独立弹层,不跟随左侧筛选)
|
|
134
|
+
Vue.watch(() => props.visible, v => { if (v) run(false); });
|
|
135
|
+
|
|
136
|
+
// 列头排序:同一个键再点一次翻转方向;默认就是服务端给的「合计从大到小」。
|
|
137
|
+
// 数字列首点从大到小(这一列就是用来找最大的),agent 名首点正序(找名字没人想倒着找)。
|
|
138
|
+
const key = Vue.ref('totalBytes'), desc = Vue.ref(true);
|
|
139
|
+
function sortBy(k) { if (key.value === k) desc.value = !desc.value; else { key.value = k; desc.value = k !== 'agent'; } }
|
|
140
|
+
const arrow = k => key.value === k ? (desc.value ? '▾' : '▴') : '';
|
|
141
|
+
const sortedRows = Vue.computed(() => {
|
|
142
|
+
const k = key.value, s = desc.value ? -1 : 1;
|
|
143
|
+
return us.rows.slice().sort((a, b) => (k === 'agent'
|
|
144
|
+
? String(a.agent).localeCompare(String(b.agent)) * s
|
|
145
|
+
: (a[k] - b[k]) * s));
|
|
146
|
+
});
|
|
147
|
+
const maxTotal = Vue.computed(() => us.rows.reduce((m, r) => Math.max(m, r.totalBytes), 0));
|
|
148
|
+
const barPct = v => maxTotal.value ? Math.max(1, Math.round(v / maxTotal.value * 100)) : 0;
|
|
149
|
+
const b = n => {
|
|
150
|
+
const v = Number(n) || 0;
|
|
151
|
+
if (v < 1024) return v + 'B';
|
|
152
|
+
if (v < 1048576) return (v / 1024).toFixed(1) + 'KB';
|
|
153
|
+
if (v < 1073741824) return (v / 1048576).toFixed(1) + 'MB';
|
|
154
|
+
return (v / 1073741824).toFixed(2) + 'GB';
|
|
155
|
+
};
|
|
156
|
+
return { us, run, key, sortBy, arrow, sortedRows, barPct, b, fmtN, fmtTime, agentColor, agentIcon };
|
|
157
|
+
}
|
|
158
|
+
};
|
package/page/log-card.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// 请求日志的一张卡片(R25 批 4):顶行 + 三格指标 + 脚注 + 展开后的详情。
|
|
2
|
+
// 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
|
|
3
|
+
//
|
|
4
|
+
// 为什么详情没有再拆一个 aa-log-detail(文档原计划里有):详情要读的那一份状态(secs / callOpen /
|
|
5
|
+
// toolJsonFold / fullIds / detail)卡片这边也得读,拆两个文件就只有两种下场 —— 卡片把七八个 props
|
|
6
|
+
// 原样转手一遍(文档明令禁止的「聪明透传」),或者走 provide/inject 把 page-check 的可用面查成一个瞎子。
|
|
7
|
+
// 合成一个文件 220 行,离 600 行的预算还早,于是按「一个卡片一个组件」落地。
|
|
8
|
+
//
|
|
9
|
+
// ⚠ v-memo 的依赖表(根里的 cardMemo)**不在这个文件里**,它挂在 aa-log-view 的 v-for 上:
|
|
10
|
+
// memo 判的是「这次要不要重渲染这一整棵子树」,跳过了连 props 都不会更新。所以**卡片模板新增读任何
|
|
11
|
+
// 响应式状态,都必须同步给 cardMemo 加一项**,漏一个就是「点了没反应」——这条注释在 aa-log-view 里也有一份。
|
|
12
|
+
window.AACTA = window.AACTA || {};
|
|
13
|
+
AACTA.LogCard = {
|
|
14
|
+
name: 'aa-log-card',
|
|
15
|
+
template: `
|
|
16
|
+
<div :id="'card-' + e.id" class="card" :class="{sel: expanded, failed: e.status!=='ok'}" @click="$emit('toggle', e.id)">
|
|
17
|
+
<div class="card-top">
|
|
18
|
+
<img v-if="iconOf(e)" class="avatar-img" :src="iconOf(e)">
|
|
19
|
+
<span v-else class="avatar" :style="{background: agentColor(e.agent)}">{{ agentLetter(e.agent) }}</span>
|
|
20
|
+
<div class="title-block">
|
|
21
|
+
<div class="title-line">
|
|
22
|
+
<b>{{ titleOf(e) }}</b>
|
|
23
|
+
<span class="mini-tag">{{ e.agent }}</span>
|
|
24
|
+
<span v-if="e.nollm" class="mini-tag" style="color:var(--placeholder)">非 LLM</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="sub-line">{{ e.preview || '(无文本预览)' }}</div>
|
|
27
|
+
<!-- R33 全文搜索的命中片段(只在搜索态、且这一条确实命中正文时有)。为什么不能省:
|
|
28
|
+
preview 是**用户输入**的前 300 字,而全文搜索多半命中的是 AI 回复或工具返回 ——
|
|
29
|
+
片段不摆出来的话,用户看着卡片上那段毫不相干的预览会以为「这条为什么算命中」。
|
|
30
|
+
三段分开渲染而不是拼 HTML:服务端只回原文,转义交给 Vue 的 {{ }},谁也不碰 innerHTML。 -->
|
|
31
|
+
<div v-if="e._snip" class="snip-line">
|
|
32
|
+
<span v-if="e._snip.pre">…{{ e._snip.pre }}</span><mark>{{ e._snip.hit }}</mark><span v-if="e._snip.post">{{ e._snip.post }}…</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="tags-right">
|
|
36
|
+
<span class="tag" :class="e.status==='ok' ? 'tag-ok' : 'tag-err'">{{ e.status==='ok' ? 'OK' : 'ERR ⚠' }}</span>
|
|
37
|
+
<span v-if="e.aborted" class="tag tag-plain">已打断</span>
|
|
38
|
+
<span class="tag tag-plain">{{ fmtDur(e.dur) }}</span>
|
|
39
|
+
<!-- I17 非 LLM 时间分解:墙钟里混着挂机/隔夜,卡片主 tag 是墙钟;真实工作时间(产品自报
|
|
40
|
+
turn_duration)明显更短时补一个哑色「实际」徽章说明差额,相等时不显示免得满屏噪声。 -->
|
|
41
|
+
<span v-if="e.rdur && e.rdur < e.dur" class="tag tag-plain tag-rdur"
|
|
42
|
+
:title="'实际工作时间(产品自报,已剥掉挂机/隔夜):' + fmtDur(e.rdur) + ',墙钟为 ' + fmtDur(e.dur)">实际 {{ fmtDur(e.rdur) }}</span>
|
|
43
|
+
<!-- I9 两轮 diff:勾选这条参与对比(点一下标入对比组,凑满两条自动弹开) -->
|
|
44
|
+
<span class="cmp-pick" :class="{on: compareSel}" :title="compareSel ? '已选中,点一下取消;再选一条即可对比' : '选这条参与两轮对比(再选一条自动弹出对比)'"
|
|
45
|
+
@click.stop="$emit('compare', e)">{{ compareSel ? '✓ 已选' : '对比' }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="metrics">
|
|
50
|
+
<div class="m-group">
|
|
51
|
+
<div class="m-label">{{ isQoder(e) ? '积分' : 'Token' }}
|
|
52
|
+
<el-tooltip placement="top" effect="dark" :show-after="200">
|
|
53
|
+
<template #content>
|
|
54
|
+
<div class="tok-tip">
|
|
55
|
+
<template v-if="isQoder(e)">
|
|
56
|
+
<div class="tr"><span>本轮积分</span><span>{{ fmtCredits(e.credits) }}</span></div>
|
|
57
|
+
<div class="sep"></div>
|
|
58
|
+
<div class="tr"><span>说明</span><span>qoder 按积分计费,不暴露 token</span></div>
|
|
59
|
+
</template>
|
|
60
|
+
<template v-else>
|
|
61
|
+
<div class="tr"><span>输入</span><span>{{ (e.tin||0).toLocaleString() }}</span></div>
|
|
62
|
+
<div class="tr"><span>输出</span><span>{{ (e.tout||0).toLocaleString() }}</span></div>
|
|
63
|
+
<div class="tr"><span>缓存命中</span><span>{{ (e.tcache||0).toLocaleString() }}</span></div>
|
|
64
|
+
<div class="sep"></div>
|
|
65
|
+
<div class="tr"><span>合计</span><span>{{ (e.total||0).toLocaleString() }}</span></div>
|
|
66
|
+
</template>
|
|
67
|
+
<!-- I12 压缩画像:与计费口径无关,qoder / token 两种卡片共用这一块。
|
|
68
|
+
「丢多少 / 等多久」只有源里真有这一位才说(compDrop 是 claude 独有),
|
|
69
|
+
缺席不补 0 —— 否则会把「没记」读成「压了却没丢」。 -->
|
|
70
|
+
<template v-if="e.compacts">
|
|
71
|
+
<div class="sep"></div>
|
|
72
|
+
<div class="tr"><span>压缩次数</span><span>{{ e.compacts }}<template v-if="e.compAuto || e.compManual">(自动 {{ e.compAuto }} / 手动 {{ e.compManual }})</template></span></div>
|
|
73
|
+
<div class="tr" v-if="e.compPre"><span>压缩前 token</span><span>{{ (e.compPre||0).toLocaleString() }}</span></div>
|
|
74
|
+
<div class="tr" v-if="e.compPost"><span>压缩后 token</span><span>{{ (e.compPost||0).toLocaleString() }}</span></div>
|
|
75
|
+
<div class="tr" v-if="e.compDrop"><span>丢弃 token</span><span>{{ (e.compDrop||0).toLocaleString() }}</span></div>
|
|
76
|
+
<div class="tr" v-if="e.compMs"><span>压缩等待</span><span>{{ fmtDur(e.compMs) }}</span></div>
|
|
77
|
+
</template>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
<!-- 图标是全局注册的组件(根 script 把 ElementPlusIconsVue 全登记了)。kebab-case 是沿用改前
|
|
81
|
+
in-DOM 时代的写法(那时浏览器会把 <QuestionFilled> 降成 <questionfilled>);现在这段是
|
|
82
|
+
JS 字符串模板,标签名不再被小写化,两种写法 Vue 都按 camelize + 首字母大写去查注册表。 -->
|
|
83
|
+
<el-icon class="tok-q"><question-filled></question-filled></el-icon>
|
|
84
|
+
</el-tooltip>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="m-vals">
|
|
87
|
+
<template v-if="isQoder(e)">
|
|
88
|
+
<span class="m-val hi"><el-icon><Coin></Coin></el-icon>{{ fmtCredits(e.credits) }} <small>积分</small></span>
|
|
89
|
+
<span v-if="e.compacts" class="m-val"><el-icon><Refresh></Refresh></el-icon>{{ e.compacts }} <small>压缩</small></span>
|
|
90
|
+
</template>
|
|
91
|
+
<template v-else-if="e.total || e.tin || e.tout || e.tcache">
|
|
92
|
+
<span class="m-val"><el-icon><Top></Top></el-icon>{{ fmtN(e.tin) }}</span>
|
|
93
|
+
<span class="m-val"><el-icon><Bottom></Bottom></el-icon>{{ fmtN(e.tout) }}</span>
|
|
94
|
+
<span class="m-val hi"><el-icon><Coin></Coin></el-icon>{{ fmtN(e.total) }}</span>
|
|
95
|
+
<span class="m-val"><el-icon><Files></Files></el-icon>{{ fmtN(e.tcache) }}</span>
|
|
96
|
+
<!-- I12 压缩徽章:claude 系以前只在 qoder 分支里印,现在压过就印(tooltip 里是明细) -->
|
|
97
|
+
<span v-if="e.compacts" class="m-val"><el-icon><Refresh></Refresh></el-icon>{{ e.compacts }} <small>压缩</small></span>
|
|
98
|
+
</template>
|
|
99
|
+
<!-- 一家源里没有逐轮 token(comate 只记上下文占用)就整栏留空,不印「0 0 0 0」:
|
|
100
|
+
四个 0 读起来像「这轮没花钱」,而真相是「这个产品没记」—— 与下面 ctx 那栏
|
|
101
|
+
「宁可少一根条,也不拿假分母凑」是同一条约定(hermes 日志滚掉时同理)。 -->
|
|
102
|
+
<span v-else class="m-val" style="color:var(--placeholder);font-weight:400">—</span>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="m-group">
|
|
106
|
+
<div class="m-label">上下文</div>
|
|
107
|
+
<div class="m-vals">
|
|
108
|
+
<!-- qoder:token 恒为 0 没有占用可显,改显最后一次调用的 context_usage_ratio(进度条分母=1) -->
|
|
109
|
+
<template v-if="isQoder(e)">
|
|
110
|
+
<span class="m-val">{{ fmtPct(e.ctxRatio) }}</span>
|
|
111
|
+
<span class="m-val"><span class="ctx-track"><i :style="{width: Math.min(100, e.ctxRatio<=1 ? e.ctxRatio*100 : e.ctxRatio)+'%'}"></i></span></span>
|
|
112
|
+
<span v-if="e.compPre" class="m-val" style="color:var(--placeholder);font-weight:400"><small>{{ fmtN(e.compPre) }} → {{ fmtN(e.compPost) }}</small></span>
|
|
113
|
+
</template>
|
|
114
|
+
<!-- ctxUsed 有值就先把占用显示出来 —— ctx(窗口**容量**)拿不到时这就是全部信息。
|
|
115
|
+
容量只对 claude 缺(转录里没有这个字段,靠模型名查表,见服务端 CTX_WINDOWS),
|
|
116
|
+
查不到就只显示占用、不画条:宁可少一根条,也不要用一个假分母凑出一根像真的的条。 -->
|
|
117
|
+
<template v-else-if="e.ctxUsed != null || e.ctx">
|
|
118
|
+
<span class="m-val">{{ fmtN(ctxUsed(e)) }} <small v-if="e.ctx">/ {{ fmtN(e.ctx) }}</small></span>
|
|
119
|
+
<span v-if="e.ctx" class="m-val"><span class="ctx-track"><i :style="{width: ctxPct(e)+'%'}"></i></span> {{ ctxPct(e) }}%</span>
|
|
120
|
+
<!-- I12:压过的轮把「压缩前 → 压缩后」也摆出来(与 qoder 分支同一画法) -->
|
|
121
|
+
<span v-if="e.compPre" class="m-val" style="color:var(--placeholder);font-weight:400"><small>{{ fmtN(e.compPre) }} → {{ fmtN(e.compPost) }}</small></span>
|
|
122
|
+
</template>
|
|
123
|
+
<span v-else class="m-val" style="color:var(--placeholder);font-weight:400">—</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="m-group">
|
|
127
|
+
<div class="m-label">调用
|
|
128
|
+
<!-- 具体工具与次数收成问号悬浮层,图标位置对齐 Token 的问号(挂在栏目标签旁) -->
|
|
129
|
+
<el-tooltip v-if="toolList(e).length" placement="top" effect="dark" :show-after="200">
|
|
130
|
+
<template #content>
|
|
131
|
+
<div class="tok-tip">
|
|
132
|
+
<div class="tr" v-for="t in toolList(e)" :key="t[0]"><span class="tn">{{ t[0] }}</span><span>×{{ t[1] }}</span></div>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
135
|
+
<el-icon class="tok-q"><question-filled></question-filled></el-icon>
|
|
136
|
+
</el-tooltip>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="m-vals">
|
|
139
|
+
<span v-if="e.calls != null" class="m-val"><el-icon><Lightning></Lightning></el-icon>{{ e.calls }} <small>次 LLM</small></span>
|
|
140
|
+
<span class="m-val"><el-icon><set-up></set-up></el-icon>{{ e.tools }} <small>工具</small></span>
|
|
141
|
+
<span v-if="e.rounds != null" class="m-val"><el-icon><Timer></Timer></el-icon>{{ e.rounds }} <small>轮</small></span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="card-foot">
|
|
147
|
+
<!-- I16/D2:时间未知的轮如实标出来,不印 1970。atomcode 里这类轮本机占 44.6%
|
|
148
|
+
(.jsonl 0 字节 / turn_id 在 jsonl 里不存在),回落 updated_at 会假装它们同时发生。 -->
|
|
149
|
+
<span v-if="e.timeUnknown" class="tag-unknown" title="这一轮拿不到时间戳(源里没有可用的轮时间),不拿会话更新时刻冒充;它不参与按天统计与时间排序">时间未知</span>
|
|
150
|
+
<span v-else>{{ fmtTime(e.time) }}</span>
|
|
151
|
+
<!-- R6:session 可点击 → 会话时间线弹层(stop 防止触发卡片展开) -->
|
|
152
|
+
<span class="r"><span class="sess-link" title="查看该会话完整时间线" @click.stop="$emit('open-session', e.session, e.agent, e.project)">{{ e.project }} · {{ e.session || '(无会话)' }}</span></span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div v-if="expanded" class="detail" @click.stop>
|
|
156
|
+
<template v-if="detail">
|
|
157
|
+
<h4>用户输入 <span class="copy-btn" title="复制原文" @click="copyRaw($event, detail.user)">复制</span></h4>
|
|
158
|
+
<pre>{{ detail.user || '(空)' }}</pre>
|
|
159
|
+
<!-- R5:失败轮单独列「失败原因」块——取 assistant 里的错误文本(isApiErrorMessage / error 事件的正文都落在 assistant),没有就提示看正文 -->
|
|
160
|
+
<template v-if="e.status !== 'ok'">
|
|
161
|
+
<h4>失败原因</h4>
|
|
162
|
+
<pre class="fail-block">{{ detail.assistant || '(本轮未捕获到独立错误文本,请查看下方 AI 输出 / 工具调用中的 ✗ 标记)' }}</pre>
|
|
163
|
+
</template>
|
|
164
|
+
<template v-if="detail.assistant">
|
|
165
|
+
<h4>AI 输出 <span class="copy-btn" title="复制原文(非渲染后文本)" @click="copyRaw($event, detail.assistant)">复制</span></h4>
|
|
166
|
+
<!-- _assistantMd 是 prepDetail 在详情入库时算好缓存的:模板里直接调 renderMd 的话,
|
|
167
|
+
卡片每次重渲染(点折叠、来 SSE)都会把几十 KB 的正则渲染重跑一遍 -->
|
|
168
|
+
<div class="md-body" v-html="detail._assistantMd"></div>
|
|
169
|
+
</template>
|
|
170
|
+
<template v-if="detail.spans && detail.spans.length">
|
|
171
|
+
<h4>执行链路({{ detail.spans.length }} 个 span)</h4>
|
|
172
|
+
<div class="span-list">
|
|
173
|
+
<div v-for="(sp, i) in secs.spans.items" :key="i" class="span-row" :style="{paddingLeft: (14 + sp.depth*18) + 'px'}">
|
|
174
|
+
<span class="sn">{{ sp.name }}</span>
|
|
175
|
+
<span v-if="sp.type" class="st">{{ sp.type }}</span>
|
|
176
|
+
<span v-if="sp.error" class="serr">✗ {{ sp.error }}</span>
|
|
177
|
+
<span class="sd">{{ fmtDur(sp.dur) }}</span>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div v-if="secs.spans.more" class="more-btn sec-toggle" @click="$emit('toggle-sec', 'spans')">{{ secs.spans.open ? '收起' : '展开全部 ' + detail.spans.length + ' 个 span(还有 ' + secs.spans.rest + ' 个)' }}</div>
|
|
181
|
+
</template>
|
|
182
|
+
<!-- 卡片写着「N 次 LLM」但一条明细都没拿到(datalog 默认关,或这轮没落盘)时,把原因说出来 -->
|
|
183
|
+
<template v-if="detail.calls && detail.calls.length">
|
|
184
|
+
<h4>LLM 调用明细({{ detail.calls.length }} 次)</h4>
|
|
185
|
+
<div class="call-list">
|
|
186
|
+
<div v-for="(c, i) in secs.calls.items" :key="i" class="call-item">
|
|
187
|
+
<div class="call-row">
|
|
188
|
+
<!-- 圆点 = 这次调用有正文,点得开;没有的多半是「只发起了工具调用」 -->
|
|
189
|
+
<span class="ci"><i v-if="c.text" class="call-dot" title="这次调用有正文,可点开"></i>#{{ i + 1 }}</span>
|
|
190
|
+
<span class="cm">{{ c.model || 'generation' }}</span>
|
|
191
|
+
<!-- 这次调用发起了哪些工具 —— 上面那行数字说明不了「它干了什么」,这个能 -->
|
|
192
|
+
<span v-if="c.tools && c.tools.length" class="call-tools" :title="'这次调用发起了:' + toolText(c.tools, 99)">{{ toolText(c.tools) }}</span>
|
|
193
|
+
<span v-if="c.error" class="cerr">✗ {{ c.error }}</span>
|
|
194
|
+
<span class="cd">{{ fmtDur(c.dur) }}</span>
|
|
195
|
+
<span v-if="c.tin != null && c.model" class="ct">↑{{ fmtN(c.tin) }} · ↓{{ fmtN(c.tout) }}<template v-if="c.tcache"> · 缓存 {{ fmtN(c.tcache) }}</template></span>
|
|
196
|
+
</div>
|
|
197
|
+
<!-- 这次调用自己产出的正文(而不是整轮拼起来的那坨)。默认收起:一轮几十次调用时
|
|
198
|
+
全铺开就是十几屏。有的行没有正文 —— 那多半是纯工具调用那几次,本来就没说话。 -->
|
|
199
|
+
<template v-if="c.text">
|
|
200
|
+
<div class="tool-sec-h call-out-h" @click="$emit('fold-callout', e.id, i)">{{ callOpen.has(e.id + ':' + i) ? '▾ 本次输出' : '▸ 本次输出' }}</div>
|
|
201
|
+
<pre v-show="callOpen.has(e.id + ':' + i)" class="call-out">{{ c.text }}</pre>
|
|
202
|
+
<span v-if="c.textTrunc && !fullIds.has(e.id)" class="more-btn" @click="$emit('load-full', e.id)">本次输出已截断,查看完整内容</span>
|
|
203
|
+
</template>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div v-if="secs.calls.more" class="more-btn sec-toggle" @click="$emit('toggle-sec', 'calls')">{{ secs.calls.open ? '收起' : '展开全部 ' + detail.calls.length + ' 次(还有 ' + secs.calls.rest + ' 次)' }}</div>
|
|
207
|
+
</template>
|
|
208
|
+
<div v-else-if="detail.callsNote" class="calls-note">{{ detail.callsNote }}</div>
|
|
209
|
+
<template v-if="detail.tools && detail.tools.length">
|
|
210
|
+
<h4>工具调用({{ detail.tools.length }})</h4>
|
|
211
|
+
<div v-for="(t, i) in secs.tools.items" :key="i" class="tool-item">
|
|
212
|
+
<div class="tool-head">
|
|
213
|
+
<span class="tname">{{ t.name }}</span>
|
|
214
|
+
<span v-if="t.error" class="terr">✗ {{ t.error }}</span>
|
|
215
|
+
<span v-if="t.dur!=null" class="tdur">{{ fmtDur(t.dur) }}</span>
|
|
216
|
+
<span class="tool-copy" @click="copyRaw($event, (t.input || '') + (t.output ? '\\n' + t.output : ''))">复制</span>
|
|
217
|
+
</div>
|
|
218
|
+
<!-- 入参/返回:_inPretty/_outPretty 是 prepDetail 缓存的 prettyJson 结果(缓存缺失时回退现算);
|
|
219
|
+
折叠开关独立记录(toolJsonFold) -->
|
|
220
|
+
<template v-if="t.input">
|
|
221
|
+
<div class="tool-sec-h" @click="$emit('fold-json', e.id, i, 'in')">{{ toolJsonFold.has(e.id + ':' + i + ':in') ? '▸ 入参' : '▾ 入参' }}</div>
|
|
222
|
+
<pre v-show="!toolJsonFold.has(e.id + ':' + i + ':in')">{{ t._inPretty || prettyJson(t.input) }}</pre>
|
|
223
|
+
</template>
|
|
224
|
+
<span v-if="t.inputTrunc" class="more-btn" @click="$emit('load-full', e.id)">输入已截断,查看完整内容</span>
|
|
225
|
+
<template v-if="t.output">
|
|
226
|
+
<div class="tool-sec-h" @click="$emit('fold-json', e.id, i, 'out')">{{ toolJsonFold.has(e.id + ':' + i + ':out') ? '▸ 返回' : '▾ 返回' }}</div>
|
|
227
|
+
<pre v-show="!toolJsonFold.has(e.id + ':' + i + ':out')">{{ t._outPretty || prettyJson(t.output) }}</pre>
|
|
228
|
+
</template>
|
|
229
|
+
<span v-if="t.outputTrunc" class="more-btn" @click="$emit('load-full', e.id)">输出已截断,查看完整内容</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div v-if="secs.tools.more" class="more-btn sec-toggle" @click="$emit('toggle-sec', 'tools')">{{ secs.tools.open ? '收起' : '展开全部 ' + detail.tools.length + ' 个(还有 ' + secs.tools.rest + ' 个)' }}</div>
|
|
232
|
+
</template>
|
|
233
|
+
<!-- 非文本 part(zcode 的状态事件 / 推理锚点 / 未知类型):未知类型不静默丢弃,保留结构化 JSON -->
|
|
234
|
+
<template v-if="detail.others && detail.others.length">
|
|
235
|
+
<h4>其他事件({{ detail.others.length }})</h4>
|
|
236
|
+
<div v-for="(o, oi) in detail.others" :key="oi">
|
|
237
|
+
<div class="other-item"><span class="ot">{{ o.type }}</span><span class="od">{{ o.t ? fmtTime(o.t) : '' }}</span></div>
|
|
238
|
+
<pre v-if="o.json" class="other-json">{{ o.json }}</pre>
|
|
239
|
+
</div>
|
|
240
|
+
</template>
|
|
241
|
+
<div v-if="isLive(e.id)" class="more-btn" @click="$emit('reload-detail', e.id)">该 agent 最新一轮 · 详情会随新日志自动刷新,也可点此手动重取</div>
|
|
242
|
+
<!-- 整张卡片的「收起」。做成吸底(.detail-foot),详情十几屏高时也随时点得到 ——
|
|
243
|
+
按钮只放在末尾的话,想中途退出就得先一路滑到底,那等于没有 -->
|
|
244
|
+
<div class="detail-foot">
|
|
245
|
+
<el-button size="small" @click="$emit('export-detail', e.id, $event)">导出该轮完整详情</el-button>
|
|
246
|
+
<el-button size="small" type="primary" @click="$emit('export-repro', e.id, $event)">导出复现包</el-button>
|
|
247
|
+
<el-button size="small" @click="$emit('collapse', e.id, $event)">收起</el-button>
|
|
248
|
+
</div>
|
|
249
|
+
</template>
|
|
250
|
+
<el-skeleton v-else :rows="3" animated></el-skeleton>
|
|
251
|
+
</div>
|
|
252
|
+
</div>`,
|
|
253
|
+
props: {
|
|
254
|
+
// 条目本体:applyUpdate 会整体换对象,所以引用比较就够(cardMemo 的第一项就是它)
|
|
255
|
+
e: { type: Object, required: true },
|
|
256
|
+
// 「是不是这张卡展开着」由父层算好 —— 卡片不需要知道 expandedId 是谁
|
|
257
|
+
expanded: Boolean,
|
|
258
|
+
// 详情本体(undefined = 还没拉回来 → 骨架屏)。secs 是它派生的三个长列表视图
|
|
259
|
+
detail: { type: Object, default: null },
|
|
260
|
+
secs: { type: Object, default: null },
|
|
261
|
+
// 三份折叠开关:与「按会话浏览」那一份共用同一套根状态(同一个 Set 在两个视图里都可能出现)
|
|
262
|
+
callOpen: { type: Object, default: () => new Set() },
|
|
263
|
+
toolJsonFold: { type: Object, default: () => new Set() },
|
|
264
|
+
fullIds: { type: Object, default: () => new Set() },
|
|
265
|
+
// 两个判定函数留在根(titleOf 闭包读 isLive → liveIds),以函数形态作 props 传入
|
|
266
|
+
titleOf: { type: Function, required: true },
|
|
267
|
+
isLive: { type: Function, required: true },
|
|
268
|
+
// I9 两轮 diff:这张卡是不是已在对比选中组里(决定「对比」按钮的选中态)
|
|
269
|
+
compareSel: { type: Boolean, default: false },
|
|
270
|
+
},
|
|
271
|
+
emits: ['toggle', 'open-session', 'collapse', 'load-full', 'fold-json', 'fold-callout', 'toggle-sec', 'reload-detail', 'export-detail', 'export-repro', 'compare'],
|
|
272
|
+
setup() {
|
|
273
|
+
// 模板看不见 page/*.js 的顶层名(批 1 实测),shared.js 的 helper 要逐个转交
|
|
274
|
+
return { iconOf, agentColor, agentLetter, isQoder, fmtCredits, fmtN, fmtDur, fmtPct, fmtTime,
|
|
275
|
+
ctxUsed, ctxPct, toolList, toolText, prettyJson, copyRaw };
|
|
276
|
+
}
|
|
277
|
+
};
|