@migor/cli 0.10.9 → 0.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -1
- package/dist/dev-machine.js +8 -1
- package/dist/inspector/fromTrace.js +64 -37
- package/dist/inspector/trace-view.bundle.js +283 -40
- package/dist/inspector/trace-view.css +55 -0
- package/dist/inspector/view.js +219 -3
- package/dist/inspector-page.html +555 -45
- package/dist/inspector-routes.js +4 -1
- package/dist/panel-logic.js +275 -8
- package/dist/templates/package.json +2 -2
- package/package.json +1 -1
package/dist/inspector-page.html
CHANGED
|
@@ -62,22 +62,71 @@
|
|
|
62
62
|
aside { display: flex; flex-direction: column; overflow: hidden; }
|
|
63
63
|
#tabs, #chat-bar { flex: none; }
|
|
64
64
|
#runs, #chat { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
65
|
-
/*
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/* 右栏分层:三块(调用树 / 能力排行 / 回复)**分同一个剩余高度**,每块自己滚。
|
|
66
|
+
为什么不再「只有树撑开、其余写死 38vh」:写死的上限在只有一块要看时是**浪费**
|
|
67
|
+
(回复长时它自己那块滚,树反而只有半屏),在块数变多时又是**挤**。折叠是用户能
|
|
68
|
+
直接操作的那个旋钮 —— 收起不要的,剩下的自然变大。 */
|
|
68
69
|
section {
|
|
69
70
|
padding: 14px; font-family: var(--mono); font-size: 12px;
|
|
70
71
|
display: flex; flex-direction: column; overflow: hidden;
|
|
71
72
|
}
|
|
72
73
|
#ttl { flex: none; }
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
#panes { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
75
|
+
.pane { display: flex; flex-direction: column; min-height: 0; }
|
|
76
|
+
.pane[hidden] { display: none; }
|
|
77
|
+
.pane + .pane { border-top: 1px solid var(--border); padding-top: 6px; }
|
|
78
|
+
/* 展开的各块**按份**分剩余高度(收掉一块,另一块自然变大);
|
|
79
|
+
收起的只留标题栏 —— `flex: 0 0 auto` 是关键:它不许参与剩余空间的分配。 */
|
|
80
|
+
.pane[data-open='1'] { flex: 1 1 0; }
|
|
81
|
+
/* 回复按 1.5 份:它是这次 run 的**结论**,而树是过程 —— 平分的话跑完只剩四行正文,
|
|
82
|
+
等于逼用户先折掉别的块才发现内容被裁了。比例是弹性的(basis 为 0 ⇒ 不会撑破容器),
|
|
83
|
+
窗口再矮也只是各自更小,不会溢出到下面的用量行。 */
|
|
84
|
+
#pane-reply[data-open='1'] { flex-grow: 1.5; }
|
|
85
|
+
/* 树按 2 份。三块**都开着**时平分的话它只有 134px ≈ 三行(1440×900 实测:23 行里
|
|
86
|
+
只看得到 3 行),而树里还塞着工具条。树是这个页面存在的理由,亏待它会让人以为
|
|
87
|
+
「这条 run 就这么点东西」。取 2 而不是更极端:回复仍要比它大(结论优先)。 */
|
|
88
|
+
#pane-trace[data-open='1'] { flex-grow: 2; }
|
|
89
|
+
/* 排行是一张**有界**的表:按自己内容的高度放,长过 45% 才在它自己那块里滚。
|
|
90
|
+
给它也按份分剩余高度的话,短表下面会空出一片 —— 那块空白本该属于树或回复。 */
|
|
91
|
+
#pane-summary[data-open='1'] { flex: 0 1 auto; max-height: 45%; }
|
|
92
|
+
.pane[data-open='0'] { flex: 0 0 auto; }
|
|
93
|
+
.pane-head {
|
|
94
|
+
flex: none; display: flex; align-items: baseline; gap: 6px; width: 100%;
|
|
95
|
+
background: none; border: 0; padding: 0 0 4px; color: var(--muted);
|
|
96
|
+
font: inherit; font-family: var(--mono); font-size: 11px; text-align: left; cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
.pane-head:hover { color: var(--text); }
|
|
99
|
+
.pane-head:focus-visible { outline: 1px solid var(--ice); outline-offset: 2px; }
|
|
100
|
+
/* caret 走 CSS 生成内容(不是在 JS 里改 textContent):它是**纯装饰**,
|
|
101
|
+
与 `data-open` 这个唯一状态同源,就不会出现「箭头说开着、内容收着」。 */
|
|
102
|
+
.pane-caret { flex: none; width: 9px; color: var(--faint); }
|
|
103
|
+
.pane[data-open='1'] > .pane-head .pane-caret::before { content: '▾'; }
|
|
104
|
+
.pane[data-open='0'] > .pane-head .pane-caret::before { content: '▸'; }
|
|
105
|
+
.pane-name { color: var(--text); letter-spacing: .2px; }
|
|
106
|
+
.pane-meta { margin-left: auto; color: var(--faint); }
|
|
107
|
+
.pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
108
|
+
.pane[data-open='0'] > .pane-body { display: none; }
|
|
109
|
+
/* 调用树这一块的 body **自己不滚** —— 滚动交给 `#trace` / `#trace2`
|
|
110
|
+
(并排时是两棵各自滚的树,body 滚会把它们的表头一起带走)。
|
|
111
|
+
⚠️ 这条带 id 的那一段**必须**跟着 `[data-open='1']` 走:id 选择器压过下面那条
|
|
112
|
+
`.pane[data-open='0'] > .pane-body { display: none }`(1,1,0 vs 0,3,0)——
|
|
113
|
+
第一版没加,结果折叠后树照样铺满(面板里"收了等于没收",比不折叠更糟)。 */
|
|
114
|
+
#pane-trace[data-open='1'] > .pane-body { display: flex; flex-direction: column; overflow: hidden; }
|
|
115
|
+
#trace { flex: 1 1 auto; min-height: 120px; overflow: auto; }
|
|
116
|
+
/* 树工具条(树内检索 + 时间轴开关):不塞进 `.pane-head`(那是个 button,嵌输入框点一下
|
|
117
|
+
就折叠了)。⚠️ 基础规则必须排在 `.cmp-on #tree-bar` **之前** —— 低特异性的规则写在后面
|
|
118
|
+
会被规则引擎判成「可能不生效」(`noDescendingSpecificity`),而那条告警在本仓是失败。 */
|
|
119
|
+
#tree-bar { flex: none; display: flex; gap: 6px; align-items: center; padding-bottom: 6px; }
|
|
120
|
+
.usage { flex: none; }
|
|
121
|
+
/* 并排 A/B:两棵树各占一半、**各自独立滚**(一棵树很长时不该把另一棵推出屏幕)。
|
|
122
|
+
`#trace2` 只在并排时存在(单run 时 hidden)。 */
|
|
123
|
+
.cmp-on #pane-trace > .pane-body { flex-direction: row; flex-wrap: wrap; }
|
|
124
|
+
/* 并排时 body 是 row ⇒ 树工具条必须自己占满一整行,否则它会挤在两棵树左边。
|
|
125
|
+
单栏时 body 是 column,`flex: none` 就够(写成 100% 会变成「占满高度」)。 */
|
|
126
|
+
.cmp-on #tree-bar { flex: 0 0 100%; }
|
|
78
127
|
.cmp-on #trace { flex: 1 1 50%; }
|
|
79
128
|
.tr-compare {
|
|
80
|
-
flex: 1 1 50%; min-height:
|
|
129
|
+
flex: 1 1 50%; min-height: 120px; overflow: auto;
|
|
81
130
|
border-left: 1px solid var(--border);
|
|
82
131
|
}
|
|
83
132
|
#diffbar { flex: none; max-height: 30vh; overflow: auto; padding-bottom: 6px; }
|
|
@@ -94,7 +143,9 @@
|
|
|
94
143
|
两者可以不同 —— 正在看的可能不在对比里)。用左边框双线区分,不靠颜色。 */
|
|
95
144
|
.run.cmp { box-shadow: inset 2px 0 0 var(--warn); }
|
|
96
145
|
.run.cmp.active { box-shadow: inset 2px 0 0 var(--ice), inset 4px 0 0 var(--warn); }
|
|
97
|
-
/* 窄屏:两栏并排已经太窄 ⇒ 回到单列 +
|
|
146
|
+
/* 窄屏:两栏并排已经太窄 ⇒ 回到单列 + 整页滚动(此时「整页滚」才是唯一合理的形态)。
|
|
147
|
+
三块也回到「各自自然高度」—— 屏幕本来就装不下,再按比例分剩余高度只会让每块都变成
|
|
148
|
+
一条缝;此时折叠仍然有效(收起的照样只剩标题栏),但它不再是唯一的高度旋钮。 */
|
|
98
149
|
@media (max-width: 860px) {
|
|
99
150
|
html, body { height: auto; }
|
|
100
151
|
body { overflow: visible; }
|
|
@@ -102,9 +153,11 @@
|
|
|
102
153
|
aside { overflow: visible; }
|
|
103
154
|
section { overflow: visible; }
|
|
104
155
|
#runs, #chat, #trace, .tr-compare { overflow: visible; }
|
|
156
|
+
#panes { flex: none; }
|
|
157
|
+
.pane[data-open='1'] { flex: none; }
|
|
105
158
|
#trace, .tr-compare { min-height: 320px; }
|
|
106
159
|
.cmp-on #trace { flex: 1 1 auto; }
|
|
107
|
-
|
|
160
|
+
.usage, #diffbar { max-height: none; }
|
|
108
161
|
}
|
|
109
162
|
.run {
|
|
110
163
|
display: block; width: 100%; text-align: left; cursor: pointer;
|
|
@@ -119,10 +172,47 @@
|
|
|
119
172
|
/* 被中止的 run 不是失败:给它一个中性偏警告的颜色,不跟失败共用红色 ——
|
|
120
173
|
否则用户会把「我自己按了中止」读成「我把工程改坏了」 */
|
|
121
174
|
.run.aborted .dot { background: var(--warn); }
|
|
175
|
+
/* 等审批的 run(`suspended`):**空心点**。它既没跑完也没失败,实心点怎么上色都是
|
|
176
|
+
在替用户下结论;空心 = 「这一轮还悬着」,与右栏那句「停在审批」同一件事 */
|
|
177
|
+
.run.suspended .dot { background: transparent; box-shadow: inset 0 0 0 1px var(--warn); }
|
|
122
178
|
.run .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
123
179
|
.run .r2, .run .r3 { margin-top: 3px; color: var(--faint); font-size: 11px; font-family: var(--mono); }
|
|
124
|
-
.run .r3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
180
|
+
.run .r2, .run .r3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
125
181
|
.empty { color: var(--faint); padding: 16px; }
|
|
182
|
+
/* —— 列表检索(2026-11-13):环里 50 条 run,此前只能靠肉眼扫 —— */
|
|
183
|
+
#list-bar {
|
|
184
|
+
flex: none; display: flex; gap: 6px; padding: 6px 8px;
|
|
185
|
+
border-bottom: 1px solid var(--border);
|
|
186
|
+
}
|
|
187
|
+
#list-q {
|
|
188
|
+
flex: 1; min-width: 0; background: var(--bg); border: 1px solid var(--border);
|
|
189
|
+
border-radius: 4px; color: var(--text); padding: 4px 7px; font: inherit; font-size: 11.5px;
|
|
190
|
+
}
|
|
191
|
+
#list-q:focus { outline: 1px solid var(--ice); border-color: var(--ice); }
|
|
192
|
+
/* 「只看异常」是**开关**,用 aria-pressed 表示按下(同时给读屏软件) */
|
|
193
|
+
#list-abn[aria-pressed='true'] { border-color: var(--warn); color: var(--warn); }
|
|
194
|
+
/* —— run 级总账(2026-11-13):打开一条 run,先给「这一趟的账」,再给树 ——
|
|
195
|
+
此前页面上只有 footer 的两个数(in / out),而 cache 命中率与成本 —— 最直接对应
|
|
196
|
+
账单的两个数 —— 一个都没有。 */
|
|
197
|
+
#ledger {
|
|
198
|
+
flex: none; display: flex; align-items: baseline; gap: 10px;
|
|
199
|
+
margin-bottom: 8px; font-size: 11.5px; color: var(--muted); flex-wrap: wrap;
|
|
200
|
+
}
|
|
201
|
+
#ledger[hidden] { display: none; }
|
|
202
|
+
#ledger-items { display: flex; flex-wrap: wrap; gap: 4px 12px; }
|
|
203
|
+
#ledger-items .lg { color: var(--faint); }
|
|
204
|
+
#ledger-items .lg b { color: var(--text); font-weight: 500; margin-left: 4px; }
|
|
205
|
+
#ledger-items .lg.err b { color: var(--err); }
|
|
206
|
+
#ledger .acts { margin-left: auto; display: flex; gap: 6px; }
|
|
207
|
+
/* —— 树工具条:树内检索 + 时间轴开关 ——
|
|
208
|
+
基础规则在上面的 `#trace` 旁边(要排在 `.cmp-on` 那条之前)。 */
|
|
209
|
+
#tree-q {
|
|
210
|
+
flex: 1; min-width: 0; background: var(--bg); border: 1px solid var(--border);
|
|
211
|
+
border-radius: 4px; color: var(--text); padding: 3px 7px; font: inherit; font-size: 11px;
|
|
212
|
+
}
|
|
213
|
+
#tree-q:focus { outline: 1px solid var(--ice); border-color: var(--ice); }
|
|
214
|
+
#tree-hit { color: var(--faint); font-size: 10.5px; min-width: 3ch; }
|
|
215
|
+
#tl-toggle[aria-pressed='false'] { color: var(--faint); }
|
|
126
216
|
#ttl { color: var(--faint); margin-bottom: 10px; font-family: var(--mono); font-size: 11.5px; }
|
|
127
217
|
.usage { display: flex; gap: 16px; margin-top: 12px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
128
218
|
/* biome-ignore lint/style/noDescendingSpecificity: 本行只颜色/字重,对手是 33 行的 header .live b —— 两者祖先容器不同(.usage 在面板区、.live 在 header),结构上不可能命中同一元素,是规则「只比末段标签」的误报。 */
|
|
@@ -154,11 +244,12 @@
|
|
|
154
244
|
#chat-bar[hidden] { display: none; }
|
|
155
245
|
#chat-sid { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
|
|
156
246
|
|
|
247
|
+
/* 回复正文:它现在住在「回复」这一块里(上面有自己的标题栏),
|
|
248
|
+
所以不再要外边距,也不再自带 max-height —— 高度由块分、滚动由 `.pane-body` 出。 */
|
|
157
249
|
#reply {
|
|
158
|
-
|
|
250
|
+
padding: 8px 10px; border: 1px solid var(--border); border-radius: 4px;
|
|
159
251
|
background: #0e0e10; word-break: break-word; color: var(--muted);
|
|
160
252
|
}
|
|
161
|
-
#reply[hidden] { display: none; }
|
|
162
253
|
|
|
163
254
|
/* —— 模型正文:Markdown 渲染 ——
|
|
164
255
|
`.md` 里所有元素都是 createElement 造出来的(解析器不产生 HTML,渲染器不用 innerHTML),
|
|
@@ -282,6 +373,14 @@
|
|
|
282
373
|
<button class="tab active" id="tab-runs" type="button">run 列表</button>
|
|
283
374
|
<button class="tab" id="tab-chat" type="button">对话</button>
|
|
284
375
|
</div>
|
|
376
|
+
<!--
|
|
377
|
+
列表检索(2026-11-13):`/` 聚焦。「异常」= 失败 / 中止 / 等审批 —— 挂起的 run
|
|
378
|
+
在 trace 上状态是 ok,只过滤 ok=false 会把它漏掉(规则在 panel-logic 的 filterRuns)。
|
|
379
|
+
-->
|
|
380
|
+
<div id="list-bar">
|
|
381
|
+
<input id="list-q" type="search" spellcheck="false" placeholder="搜 prompt / 目录 / 收尾原因(按 /)" />
|
|
382
|
+
<button id="list-abn" class="mini" type="button" aria-pressed="false" title="只看失败 / 中止 / 等审批的 run">异常</button>
|
|
383
|
+
</div>
|
|
285
384
|
<div id="runs"><div class="empty">还没有 run。触发一次 agent 调用即可。</div></div>
|
|
286
385
|
<!--
|
|
287
386
|
清空对话(§6 待定 3):清空 = **换一个 sessionId**(不动 session.json ——
|
|
@@ -296,6 +395,18 @@
|
|
|
296
395
|
</aside>
|
|
297
396
|
<section>
|
|
298
397
|
<div id="ttl"></div>
|
|
398
|
+
<!--
|
|
399
|
+
run 级总账(2026-11-13):轮次 / 工具 / 失败 / in-out / cache 命中率 / 成本(带预算)
|
|
400
|
+
/ 模型。判据与文案全在 panel-logic 的 `runLedger`(带单测),这里只填 DOM。
|
|
401
|
+
右侧两个动作:重跑(回填四个输入,**不自动跑**)与复制 JSONL(喂给 `agentia view`)。
|
|
402
|
+
-->
|
|
403
|
+
<div id="ledger" hidden>
|
|
404
|
+
<div id="ledger-items"></div>
|
|
405
|
+
<div class="acts">
|
|
406
|
+
<button class="mini" id="rerun" type="button" title="把这条 run 的 prompt / 目录 / 能力 / 多轮回填到输入条(不会自动开跑)">↻ 回填重跑</button>
|
|
407
|
+
<button class="mini" id="copy-run" type="button" title="复制这一条 trace 的 JSONL(存成 .jsonl 后可用 agentia view 打开)">复制 JSONL</button>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
299
410
|
<!--
|
|
300
411
|
并排 A/B 的差异摘要(档 B)。默认 hidden,只有选满两条 run 才出现。
|
|
301
412
|
三段结构:可比性提示(不可比时才有 —— 可比时**不显示**,见 comparability 的注释)、
|
|
@@ -306,14 +417,56 @@
|
|
|
306
417
|
<div id="diff-body"></div>
|
|
307
418
|
</div>
|
|
308
419
|
<!--
|
|
309
|
-
|
|
310
|
-
|
|
420
|
+
右栏三块:调用树 / 能力排行 / 回复。**三块都能折叠**(2026-11-12 ref-panel-panes)。
|
|
421
|
+
为什么:这块栏位的高度是三者抢的 —— 三块同时展开时调用树只剩十几行、回复只剩两段,
|
|
422
|
+
读谁都不够;而多数时刻用户只在看其中一块。折叠掉不要的那两块,剩下那块自动铺满
|
|
423
|
+
(`flex` 分的是同一个剩余空间,不是三份写死的 vh)。
|
|
424
|
+
⚠️ 折叠态在标题栏留一行**规模读数**(N span / N 项 / N 行)—— 收起不等于失明。
|
|
311
425
|
-->
|
|
312
|
-
<div id="
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
426
|
+
<div id="panes">
|
|
427
|
+
<div class="pane" id="pane-trace" data-open="1">
|
|
428
|
+
<button class="pane-head" id="head-trace" type="button" aria-expanded="true" aria-controls="body-trace">
|
|
429
|
+
<span class="pane-caret" aria-hidden="true"></span>
|
|
430
|
+
<span class="pane-name">调用树</span>
|
|
431
|
+
<span class="pane-meta" id="meta-trace"></span>
|
|
432
|
+
</button>
|
|
433
|
+
<div class="pane-body" id="body-trace">
|
|
434
|
+
<!--
|
|
435
|
+
树工具条(2026-11-13):树内检索 + 时间轴开关。
|
|
436
|
+
检索**只高亮 + 跳转,不改结构**(裁剪节点会让「谁挂在谁下面」失真);
|
|
437
|
+
判定与跳转都在 trace-view 里(`setQuery` / `nextHit`),这里只接线。
|
|
438
|
+
-->
|
|
439
|
+
<div id="tree-bar">
|
|
440
|
+
<input id="tree-q" type="search" spellcheck="false" placeholder="树里找…(Enter 下一处 / Esc 清空)" />
|
|
441
|
+
<span id="tree-hit"></span>
|
|
442
|
+
<button class="mini" id="tl-toggle" type="button" aria-pressed="true" title="按 run 总时长归一的时间轴(关掉可以给它让出 72px)">时间轴</button>
|
|
443
|
+
</div>
|
|
444
|
+
<!--
|
|
445
|
+
双栏容器:并排时这里是**两个** trace 视图,单run 时只有一个。
|
|
446
|
+
⚠️ 不用「同一棵树改CSS 成两半」—— trace-view 的树是自绘的,两棵树必须两个容器。
|
|
447
|
+
-->
|
|
448
|
+
<div id="trace" class="tr-single"><div class="tr-empty">// 选一个 run 查看调用树</div></div>
|
|
449
|
+
<div id="trace2" class="tr-compare" hidden></div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
<!-- 能力排行(G2):哪个能力慢 / 贵 / 爱失败 —— 调优从这里下手 -->
|
|
453
|
+
<div class="pane" id="pane-summary" data-open="1" hidden>
|
|
454
|
+
<button class="pane-head" id="head-summary" type="button" aria-expanded="true" aria-controls="body-summary">
|
|
455
|
+
<span class="pane-caret" aria-hidden="true"></span>
|
|
456
|
+
<span class="pane-name">能力排行</span>
|
|
457
|
+
<span class="pane-meta" id="meta-summary"></span>
|
|
458
|
+
</button>
|
|
459
|
+
<div class="pane-body" id="body-summary"><div id="summary"></div></div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="pane" id="pane-reply" data-open="1" hidden>
|
|
462
|
+
<button class="pane-head" id="head-reply" type="button" aria-expanded="true" aria-controls="body-reply">
|
|
463
|
+
<span class="pane-caret" aria-hidden="true"></span>
|
|
464
|
+
<span class="pane-name">回复</span>
|
|
465
|
+
<span class="pane-meta" id="meta-reply"></span>
|
|
466
|
+
</button>
|
|
467
|
+
<div class="pane-body" id="body-reply"><div id="reply"></div></div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
317
470
|
<div class="usage" id="usage" hidden>
|
|
318
471
|
<span>in <b id="u-in">0</b></span>
|
|
319
472
|
<span>out <b id="u-out">0</b></span>
|
|
@@ -382,6 +535,7 @@
|
|
|
382
535
|
diffValueText,
|
|
383
536
|
diffFetchErrorText,
|
|
384
537
|
emptyTraceAccumulator,
|
|
538
|
+
filterRuns,
|
|
385
539
|
filterSelected,
|
|
386
540
|
formatToolSources,
|
|
387
541
|
historyStep,
|
|
@@ -394,9 +548,16 @@
|
|
|
394
548
|
promptAfterFilePick,
|
|
395
549
|
pushPromptHistory,
|
|
396
550
|
replyBelongsTo,
|
|
551
|
+
rerunFromNote,
|
|
397
552
|
runDoneNotice,
|
|
398
|
-
|
|
553
|
+
runIdFromSearch,
|
|
554
|
+
runLedger,
|
|
399
555
|
shortenPath,
|
|
556
|
+
replyMeta,
|
|
557
|
+
stopReasonClass,
|
|
558
|
+
stopReasonText,
|
|
559
|
+
summaryMeta,
|
|
560
|
+
traceMeta,
|
|
400
561
|
toggleCompare,
|
|
401
562
|
turnKey,
|
|
402
563
|
} from './panel-logic.js';
|
|
@@ -433,6 +594,15 @@
|
|
|
433
594
|
const fsEl = $('fs');
|
|
434
595
|
const fsListEl = $('fs-list');
|
|
435
596
|
const fsPathEl = $('fs-path');
|
|
597
|
+
const listQEl = $('list-q');
|
|
598
|
+
const listAbnEl = $('list-abn');
|
|
599
|
+
const ledgerEl = $('ledger');
|
|
600
|
+
const ledgerItemsEl = $('ledger-items');
|
|
601
|
+
const rerunEl = $('rerun');
|
|
602
|
+
const copyRunEl = $('copy-run');
|
|
603
|
+
const treeQEl = $('tree-q');
|
|
604
|
+
const treeHitEl = $('tree-hit');
|
|
605
|
+
const tlToggleEl = $('tl-toggle');
|
|
436
606
|
|
|
437
607
|
const view = createTraceView(traceEl, {
|
|
438
608
|
// 无公开单价 → 不估算成本;只展示 token
|
|
@@ -517,6 +687,9 @@
|
|
|
517
687
|
const t = await res.json();
|
|
518
688
|
el.replaceChildren();
|
|
519
689
|
playTrace(v, t);
|
|
690
|
+
// 树内检索是**跨重画**的状态(它在 view 里),但 playTrace 只是重画;
|
|
691
|
+
// 这里补一次是为了「并排时新打开的那一条也按当前关键词高亮」(否则两栏不同步)
|
|
692
|
+
if (treeQEl.value.trim()) v.setQuery(treeQEl.value);
|
|
520
693
|
}
|
|
521
694
|
await loadDiff(a, b);
|
|
522
695
|
}
|
|
@@ -641,6 +814,11 @@
|
|
|
641
814
|
live: null,
|
|
642
815
|
notice: '', // 面板自己发的操作反馈(已接受 / 重启了 runner / 出错)
|
|
643
816
|
noticeErr: false,
|
|
817
|
+
/** 列表检索(2026-11-13):关键词 **且** 「只看异常」—— 两者都与列表同生共死 */
|
|
818
|
+
q: '',
|
|
819
|
+
onlyAbnormal: false,
|
|
820
|
+
/** 当前打开的那条 trace 本体(总账 / 回填重跑 / 导出都用它;`state.current` 只有 id) */
|
|
821
|
+
trace: null,
|
|
644
822
|
};
|
|
645
823
|
|
|
646
824
|
const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
|
|
@@ -768,18 +946,42 @@
|
|
|
768
946
|
runsEl.appendChild(d);
|
|
769
947
|
return;
|
|
770
948
|
}
|
|
949
|
+
// 检索(2026-11-13):关键词 **且** 「只看异常」。判定在 panel-logic(带单测)——
|
|
950
|
+
// 这里的「异常」包含挂起(它的 trace 状态是 ok,只过滤 ok=false 会漏掉)。
|
|
951
|
+
const shown = filterRuns(state.list, state.q, { onlyAbnormal: state.onlyAbnormal });
|
|
771
952
|
runsEl.replaceChildren();
|
|
772
|
-
|
|
953
|
+
if (shown.length === 0) {
|
|
954
|
+
// ⚠️ 「过滤掉了全部」与「一条都没有」是两件事,必须说清是哪种 ——
|
|
955
|
+
// 一个错误的空态比一句错误更难发现(用户会以为 run 丢了)
|
|
956
|
+
const d = document.createElement('div');
|
|
957
|
+
d.className = 'empty';
|
|
958
|
+
const why = state.onlyAbnormal ? '只看异常' : '无筛选';
|
|
959
|
+
d.textContent = `没有匹配的 run(共 ${state.list.length} 条 · ${why}${state.q ? ` · 关键词「${state.q}」` : ''})`;
|
|
960
|
+
runsEl.appendChild(d);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
for (const r of shown) {
|
|
773
964
|
const b = document.createElement('button');
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
|
|
965
|
+
// 四态:失败 / **被中止** / **等审批** / 正常。判别全走 panel-logic 的
|
|
966
|
+
// `stopReasonClass`(与通知条同一条语义)—— 各处自己读 `ok` 会出现「列表说失败、
|
|
967
|
+
// 通知条说已中止」这种同一件事两个结论。
|
|
968
|
+
// 为什么等审批要单独一态:`suspended` 的 trace 状态是 ok(挂起段本身没出错)
|
|
969
|
+
// ⇒ 只看 ok 它是一个**正常跑完的蓝点**,而那一轮其实还挂着等人。
|
|
970
|
+
const cls = stopReasonClass(r);
|
|
971
|
+
const tone =
|
|
972
|
+
cls === 'failed'
|
|
973
|
+
? ' err'
|
|
974
|
+
: cls === 'aborted'
|
|
975
|
+
? ' aborted'
|
|
976
|
+
: cls === 'suspended'
|
|
977
|
+
? ' suspended'
|
|
978
|
+
: '';
|
|
778
979
|
b.className =
|
|
779
980
|
'run' +
|
|
780
981
|
tone +
|
|
781
982
|
(r.traceId === state.current ? ' active' : '') +
|
|
782
983
|
(state.compare.includes(r.traceId) ? ' cmp' : '');
|
|
984
|
+
|
|
783
985
|
b.type = 'button';
|
|
784
986
|
|
|
785
987
|
const r1 = document.createElement('span');
|
|
@@ -788,12 +990,22 @@
|
|
|
788
990
|
dot.className = 'dot';
|
|
789
991
|
const name = document.createElement('span');
|
|
790
992
|
name.className = 'name';
|
|
791
|
-
|
|
993
|
+
// 首行给**这次 run 到底问了什么**。此前这里是 `r.name`,而 dev 环起的 run
|
|
994
|
+
// 一律叫 `agent.run`(`engine/loop.ts` 的缺省,dev-runner 不传 runName)
|
|
995
|
+
// ⇒ 五十条一模一样的条目,只能靠第二行的时间戳认人。
|
|
996
|
+
// prompt 是模型不可控的**用户输入**,仍走 textContent;多行只取首行(CSS 再省略)。
|
|
997
|
+
const prompt = (r.note && typeof r.note.prompt === 'string' ? r.note.prompt : '').trim();
|
|
998
|
+
name.textContent = prompt ? prompt.split('\n')[0] : r.name;
|
|
999
|
+
// 全文进 title:列表里被省略的那半句常常正是区分两次 run 的部分
|
|
1000
|
+
b.title = prompt || r.name;
|
|
792
1001
|
r1.append(dot, name);
|
|
793
1002
|
|
|
794
1003
|
const r2 = document.createElement('span');
|
|
795
1004
|
r2.className = 'r2';
|
|
796
|
-
|
|
1005
|
+
// 非正常收尾把**原因**写在第二行:`budget_exceeded` / `max_iterations` 的 run
|
|
1006
|
+
// 与 `end_turn` 的 run 在旧版看起来一模一样(同样蓝点亮着),而前者根本没跑完。
|
|
1007
|
+
const why = cls === 'ok' ? '' : ` · ${stopReasonText(r.stopReason)}`;
|
|
1008
|
+
r2.textContent = `${fmtMs(r.ms)} · ${fmtNum(r.tokens)} tok · ${ago(r.startedAt)}${why}`;
|
|
797
1009
|
|
|
798
1010
|
b.append(r1, r2);
|
|
799
1011
|
|
|
@@ -826,6 +1038,150 @@
|
|
|
826
1038
|
}
|
|
827
1039
|
}
|
|
828
1040
|
|
|
1041
|
+
// ---------- run 级总账(2026-11-13) ----------
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* 打开一条 run,先给「这一趟的账」:轮次 / 工具 / 失败 / in-out / cache 命中率 /
|
|
1045
|
+
* 成本(带上限)/ 模型。判据与文案全在 `runLedger`(带单测),这里只填 DOM。
|
|
1046
|
+
*
|
|
1047
|
+
* 一条可显示项都没有时**整行不出现**(不做空壳)—— 一个只有两个按钮的空账行
|
|
1048
|
+
* 会让人以为数据没读到。
|
|
1049
|
+
*/
|
|
1050
|
+
function renderLedger(trace) {
|
|
1051
|
+
ledgerItemsEl.replaceChildren();
|
|
1052
|
+
const items = runLedger(trace, state.dev ? state.dev.budget : null);
|
|
1053
|
+
ledgerEl.hidden = items.length === 0;
|
|
1054
|
+
for (const it of items) {
|
|
1055
|
+
const s = document.createElement('span');
|
|
1056
|
+
s.className = 'lg' + (it.tone === 'err' ? ' err' : '');
|
|
1057
|
+
if (it.title) s.title = it.title;
|
|
1058
|
+
s.appendChild(document.createTextNode(it.key));
|
|
1059
|
+
const b = document.createElement('b');
|
|
1060
|
+
b.textContent = it.value;
|
|
1061
|
+
s.appendChild(b);
|
|
1062
|
+
ledgerItemsEl.appendChild(s);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* 「回填重跑」:把这条 run 的四个输入写回输入条,**不自动提交**。
|
|
1068
|
+
* 自动跑会在误触时花钱,而「回填 + 我按运行」多的只是一次点击。
|
|
1069
|
+
*/
|
|
1070
|
+
rerunEl.addEventListener('click', () => {
|
|
1071
|
+
const fill = rerunFromNote(
|
|
1072
|
+
state.trace ? state.trace.note : null,
|
|
1073
|
+
state.dev ? state.dev.capabilities : [],
|
|
1074
|
+
);
|
|
1075
|
+
if (!fill) {
|
|
1076
|
+
setNotice('这条 run 没有开发期元信息(不是 agentia dev 起的,或记账已被淘汰)⇒ 没法回填', true);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
promptEl.value = fill.prompt;
|
|
1080
|
+
if (fill.workdir) {
|
|
1081
|
+
workdirEl.value = fill.workdir;
|
|
1082
|
+
state.workdir = fill.workdir;
|
|
1083
|
+
}
|
|
1084
|
+
state.selected = fill.toolSources;
|
|
1085
|
+
state.multiTurn = fill.multiTurn;
|
|
1086
|
+
renderControls();
|
|
1087
|
+
renderChat();
|
|
1088
|
+
// banner 是 textContent(不跑 Markdown)⇒ 别写 `**没有**`,那会原样露出星号
|
|
1089
|
+
setNotice('已回填这条 run 的四个输入(没有自动运行)—— 确认后按「▶ 运行」', false);
|
|
1090
|
+
promptEl.focus();
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* 复制这一条 trace 的 JSONL。去掉 CLI 侧的 `note`:它不是 trace 的一部分,
|
|
1095
|
+
* 而产物的去处是 `agentia view <file>`(那份只认 trace 形状)。
|
|
1096
|
+
*/
|
|
1097
|
+
copyRunEl.addEventListener('click', async () => {
|
|
1098
|
+
if (!state.current) return;
|
|
1099
|
+
try {
|
|
1100
|
+
const res = await fetch('/api/runs/' + encodeURIComponent(state.current));
|
|
1101
|
+
if (!res.ok) {
|
|
1102
|
+
setNotice(`读 trace 失败:HTTP ${res.status}`, true);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const trace = await res.json();
|
|
1106
|
+
const bare = { ...trace };
|
|
1107
|
+
delete bare.note;
|
|
1108
|
+
const text = JSON.stringify(bare) + '\n';
|
|
1109
|
+
const clip = navigator.clipboard;
|
|
1110
|
+
if (!clip || typeof clip.writeText !== 'function') {
|
|
1111
|
+
setNotice('这个浏览器没有剪贴板接口 —— 可以直接用 agentia view 读 .agentia/traces.jsonl', true);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
await clip.writeText(text);
|
|
1115
|
+
setNotice(`已复制 ${text.length} 字符。存成 .jsonl 后:agentia view <文件>`, false);
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
setNotice(`复制失败:${e.message}`, true);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// ---------- 右栏三块的折叠(收起不要的,剩下的自动变大) ----------
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* 折叠态是**布局偏好**,不是数据:切 run / 重新打开页面都不该把它重置
|
|
1125
|
+
* (每次回来都「三块全开、谁都不够高」等于没做)。所以它进 localStorage。
|
|
1126
|
+
* 存不下(隐私模式 / 配额满 / 被禁用)就只活这一次会话 —— 面板不因为记不住布局而白屏,
|
|
1127
|
+
* 这也是下面那个 `try` 存在的唯一理由。
|
|
1128
|
+
*/
|
|
1129
|
+
const PANES = ['trace', 'summary', 'reply'];
|
|
1130
|
+
const PANE_KEY = 'agentia.panes';
|
|
1131
|
+
const paneEls = {};
|
|
1132
|
+
for (const id of PANES) {
|
|
1133
|
+
paneEls[id] = { box: $(`pane-${id}`), head: $(`head-${id}`), meta: $(`meta-${id}`) };
|
|
1134
|
+
}
|
|
1135
|
+
const openPanes = (() => {
|
|
1136
|
+
try {
|
|
1137
|
+
const raw = JSON.parse(localStorage.getItem(PANE_KEY) || 'null');
|
|
1138
|
+
return new Set(Array.isArray(raw) ? raw.filter((id) => PANES.includes(id)) : PANES);
|
|
1139
|
+
} catch {
|
|
1140
|
+
return new Set(PANES);
|
|
1141
|
+
}
|
|
1142
|
+
})();
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* 写折叠态的**唯一**入口(含初始化)。
|
|
1146
|
+
*
|
|
1147
|
+
* `data-open`(视觉)与 `aria-expanded`(语义)必须一起改 —— 只改前者,屏幕阅读器读到的
|
|
1148
|
+
* 还是「展开」;只改后者,屏幕上是收着的。两条同源的守卫在 `test/structure.test.mjs`
|
|
1149
|
+
* (W6:`dataset.open =` 全文件只许出现一次)。
|
|
1150
|
+
*/
|
|
1151
|
+
function applyPane(id, open) {
|
|
1152
|
+
const p = paneEls[id];
|
|
1153
|
+
p.box.dataset.open = open ? '1' : '0';
|
|
1154
|
+
p.head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1155
|
+
if (open) openPanes.add(id);
|
|
1156
|
+
else openPanes.delete(id);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** 用户点击(或初始化之后)才落盘:`applyPane` 在初始化时也会跑,不该顺手写一遍盘 */
|
|
1160
|
+
function persistPanes() {
|
|
1161
|
+
try {
|
|
1162
|
+
localStorage.setItem(PANE_KEY, JSON.stringify([...openPanes]));
|
|
1163
|
+
} catch {
|
|
1164
|
+
/* 存不下就算了:这一次会话内折叠仍然有效 */
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* 标题栏读数。**只在真的变了时**写 —— 在飞时每一帧都会调它,无谓的赋值会白触发重排
|
|
1170
|
+
* (调用树那块的读数是随 span 数增长的,帧率下这个差别指的是真的会掉帧)。
|
|
1171
|
+
*/
|
|
1172
|
+
function setPaneMeta(id, text) {
|
|
1173
|
+
const el = paneEls[id].meta;
|
|
1174
|
+
if (el.textContent !== text) el.textContent = text;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
for (const id of PANES) {
|
|
1178
|
+
paneEls[id].head.addEventListener('click', () => {
|
|
1179
|
+
applyPane(id, paneEls[id].box.dataset.open !== '1');
|
|
1180
|
+
persistPanes();
|
|
1181
|
+
});
|
|
1182
|
+
applyPane(id, openPanes.has(id));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
829
1185
|
// ---------- 模型正文的渲染(Markdown + 长了就折叠) ----------
|
|
830
1186
|
|
|
831
1187
|
/**
|
|
@@ -960,17 +1316,22 @@
|
|
|
960
1316
|
* 折叠控件放在正文**下方、左对齐**,不在行尾悬 caret:trace-view 那边 caret 靠右是因为
|
|
961
1317
|
* 行是 nowrap 的表格状布局,「右端」本来就是「这一行结束了」的位置;正文块不是那个形态,
|
|
962
1318
|
* 一个 10px 的箭头挂在右上角没人会去找。判定走 `collapseDecision`(纯逻辑、带单测)。
|
|
1319
|
+
*
|
|
1320
|
+
* `opts.clamp === false` 关掉**内层**的高度夹(回复那一块用):那块自己已经能折叠、
|
|
1321
|
+
* 自己会滚,再在里面夹一刀就出现两个「展开」—— 一个收整块、一个收正文,点错哪个都
|
|
1322
|
+
* 不像自己期望的。对话轮仍要夹:它长在一列滚滚条里,一条长回复能把整列顶掉。
|
|
963
1323
|
*/
|
|
964
|
-
function renderMessage(wrap, text, key) {
|
|
1324
|
+
function renderMessage(wrap, text, key, opts = {}) {
|
|
965
1325
|
wrap.classList.add('md-wrap');
|
|
966
1326
|
const body = document.createElement('div');
|
|
967
1327
|
body.className = 'md';
|
|
968
1328
|
const d = collapseDecision(text);
|
|
1329
|
+
const clamp = opts.clamp !== false;
|
|
969
1330
|
const open = expandedMsgs.has(key);
|
|
970
|
-
if (d.collapsed && !open) body.classList.add('md-clamped');
|
|
1331
|
+
if (clamp && d.collapsed && !open) body.classList.add('md-clamped');
|
|
971
1332
|
renderMarkdownInto(body, text);
|
|
972
1333
|
wrap.appendChild(body);
|
|
973
|
-
if (!d.collapsed) return; // 短正文不挂控件(挂一个「展开」是噪声)
|
|
1334
|
+
if (!clamp || !d.collapsed) return; // 短正文不挂控件(挂一个「展开」是噪声)
|
|
974
1335
|
const btn = document.createElement('button');
|
|
975
1336
|
btn.type = 'button';
|
|
976
1337
|
btn.className = 'md-toggle';
|
|
@@ -984,15 +1345,19 @@
|
|
|
984
1345
|
wrap.appendChild(btn);
|
|
985
1346
|
}
|
|
986
1347
|
|
|
987
|
-
/**
|
|
1348
|
+
/**
|
|
1349
|
+
* 右栏「回复」那一块。key 带 traceId ⇒ 换一轮就是一个新的折叠态。
|
|
1350
|
+
*
|
|
1351
|
+
* 没有回复时**整块藏起来**(不是留个空的标题栏):不做空壳是这块面板的既有口径,
|
|
1352
|
+
* 而一个永远挂在下面的「回复 ▸」会让人以为回复被自己折起来了。
|
|
1353
|
+
*/
|
|
988
1354
|
function renderReply() {
|
|
989
1355
|
replyEl.replaceChildren();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
renderMessage(replyEl, state.reply, `reply:${state.replyFor ?? ''}`);
|
|
1356
|
+
const shown = Boolean(state.reply);
|
|
1357
|
+
paneEls.reply.box.hidden = !shown;
|
|
1358
|
+
if (!shown) return;
|
|
1359
|
+
renderMessage(replyEl, state.reply, `reply:${state.replyFor ?? ''}`, { clamp: false });
|
|
1360
|
+
setPaneMeta('reply', replyMeta(state.reply));
|
|
996
1361
|
}
|
|
997
1362
|
|
|
998
1363
|
// ---------- 对话流(只在多轮开 + 有会话文件时出现) ----------
|
|
@@ -1092,8 +1457,17 @@
|
|
|
1092
1457
|
state.replyFor = null;
|
|
1093
1458
|
renderReply();
|
|
1094
1459
|
summaryEl.replaceChildren();
|
|
1460
|
+
// 排行与回复这两块在「这一轮还没产出」时**整块不出现**(不做空壳),
|
|
1461
|
+
// 但折叠态留着 —— 下一轮跑完它们按用户原来的布局回来
|
|
1462
|
+
paneEls.summary.box.hidden = true;
|
|
1463
|
+
setPaneMeta('summary', '');
|
|
1464
|
+
setPaneMeta('trace', '');
|
|
1095
1465
|
usageEl.hidden = true;
|
|
1096
1466
|
ttlEl.textContent = '';
|
|
1467
|
+
// 总账也是**上一轮**的账 ⇒ 与回复 / 排行一起清掉(收尾后由 open() 重新填)
|
|
1468
|
+
state.trace = null;
|
|
1469
|
+
ledgerEl.hidden = true;
|
|
1470
|
+
ledgerItemsEl.replaceChildren();
|
|
1097
1471
|
const ph = document.createElement('div');
|
|
1098
1472
|
ph.className = 'tr-empty';
|
|
1099
1473
|
ph.textContent = '// 这一轮在飞 —— span 会一个个长出来(收尾时用整棵 trace 覆盖)';
|
|
@@ -1119,6 +1493,7 @@
|
|
|
1119
1493
|
if (!playTrace(view, t)) return;
|
|
1120
1494
|
ttlEl.textContent = `${t.traceId}(在飞)`;
|
|
1121
1495
|
usageEl.hidden = false;
|
|
1496
|
+
setPaneMeta('trace', traceMeta(t.spans.length));
|
|
1122
1497
|
});
|
|
1123
1498
|
}
|
|
1124
1499
|
|
|
@@ -1193,26 +1568,74 @@
|
|
|
1193
1568
|
if (!res.ok) return;
|
|
1194
1569
|
const trace = await res.json();
|
|
1195
1570
|
state.current = id;
|
|
1196
|
-
|
|
1571
|
+
// 总账要的是 trace 本体(`state.current` 只有 id)—— 重跑 / 导出也靠它
|
|
1572
|
+
state.trace = trace;
|
|
1573
|
+
renderLedger(trace);
|
|
1574
|
+
/* 深链(2026-11-13):刷新页面回到**这一条**,而不是最新那条。
|
|
1575
|
+
用 `replaceState` 不堆历史 —— 切 run 是同一页的不同视图,push 会让后退键
|
|
1576
|
+
按十几次才离开面板(那不是用户对「后退」的预期)。 */
|
|
1577
|
+
try {
|
|
1578
|
+
const u = new URL(location.href);
|
|
1579
|
+
u.searchParams.set('run', id);
|
|
1580
|
+
history.replaceState(null, '', u);
|
|
1581
|
+
} catch {
|
|
1582
|
+
/* 老浏览器 / file://:没有深链不影响其它任何功能 */
|
|
1583
|
+
}
|
|
1584
|
+
// 标题带**收尾原因**(非正常收尾时)。打开一条历史 run 时最容易犯的误读是
|
|
1585
|
+
// 「树画完了 ⇒ 这一轮跑完成了」,而它可能是撞预算停的、被中止的、或正挂着等人。
|
|
1586
|
+
// 原因取自列表的 summary(`stop_reason` 已在那里解析过,别在页面里再挖 attributes)。
|
|
1587
|
+
const sum = state.list.find((x) => x.traceId === id);
|
|
1588
|
+
const cls = sum ? stopReasonClass(sum) : 'ok';
|
|
1589
|
+
ttlEl.textContent =
|
|
1590
|
+
sum && cls !== 'ok'
|
|
1591
|
+
? `${trace.traceId} · ${stopReasonText(sum.stopReason)}`
|
|
1592
|
+
: trace.traceId;
|
|
1197
1593
|
usageEl.hidden = false;
|
|
1198
1594
|
playTrace(view, trace);
|
|
1199
|
-
|
|
1595
|
+
// 排行算一次、两处消费(表格 + 标题栏读数):标题说「3 项」而表里有 4 行是
|
|
1596
|
+
// 最难查的一类不一致 —— 同源就不会有这问题
|
|
1597
|
+
const rows = summarizeTrace(trace);
|
|
1598
|
+
summaryEl.innerHTML = renderSummary(rows);
|
|
1599
|
+
paneEls.summary.box.hidden = false;
|
|
1600
|
+
setPaneMeta('summary', summaryMeta(rows));
|
|
1601
|
+
setPaneMeta('trace', traceMeta(trace.spans ? trace.spans.length : 0));
|
|
1200
1602
|
// 回复正文只对**刚跑完的那一轮**有意义(trace 上没有它 —— 它是 run-done 事件带的)
|
|
1201
1603
|
// ⇒ 打开**别的** run 时必须清掉,否则它挂在别人的 trace 底下(张冠李戴)。
|
|
1202
1604
|
//
|
|
1203
1605
|
// ⚠️ 但不能**无条件**清:`run-done` 与「这条 trace 的收尾帧」几乎同毫秒到达,
|
|
1204
1606
|
// 而这条 handler 的链更长(refresh → refreshSession → renderChat → open)——
|
|
1205
1607
|
// 无条件清会把刚写上去的那句回复擦掉(端到端实测 17 ms,现象是「跑完了但没回复」)。
|
|
1608
|
+
//
|
|
1609
|
+
// 清完之后**立刻从 CLI 侧记账取回**:`finalText` 不在 trace 上,但它跟着 note 一起
|
|
1610
|
+
// 存着(`/api/runs/:id` 的顶层 `note`)。不取的话,切到别的 run 再切回来 ——
|
|
1611
|
+
// 甚至只是刷新一次页面 —— 那一轮的结论就永远看不到了。
|
|
1206
1612
|
if (!replyBelongsTo(id, state.replyFor)) {
|
|
1207
|
-
|
|
1208
|
-
state.
|
|
1613
|
+
const saved = trace.note && typeof trace.note.finalText === 'string' ? trace.note.finalText : '';
|
|
1614
|
+
state.reply = saved;
|
|
1615
|
+
state.replyFor = saved ? id : null;
|
|
1209
1616
|
renderReply();
|
|
1210
1617
|
}
|
|
1211
1618
|
renderList();
|
|
1212
1619
|
}
|
|
1213
1620
|
|
|
1214
|
-
|
|
1621
|
+
/**
|
|
1622
|
+
* 打开一条 run:优先 `?run=<id>`(深链 / 刷新),否则最新一条。
|
|
1623
|
+
*
|
|
1624
|
+
* 深链指向的那条**已不在内存 ring 里**时必须出声:静默回落最新那条会让用户以为
|
|
1625
|
+
* 「链接打开了,就是这条」—— 而他在看别的 run 的树(本仓最忌的张冠李戴)。
|
|
1626
|
+
*/
|
|
1627
|
+
async function openLatest(want) {
|
|
1215
1628
|
await refresh();
|
|
1629
|
+
if (want) {
|
|
1630
|
+
if (state.list.some((r) => r.traceId === want)) {
|
|
1631
|
+
await open(want);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
setNotice(
|
|
1635
|
+
`打不开 ?run=${want}:它不在内存的 run 列表里(可能已被淘汰,或不是这个面板起的)。已回到最新一条`,
|
|
1636
|
+
true,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1216
1639
|
if (state.list.length && !state.current) await open(state.list[0].traceId);
|
|
1217
1640
|
}
|
|
1218
1641
|
|
|
@@ -1355,6 +1778,92 @@
|
|
|
1355
1778
|
}
|
|
1356
1779
|
});
|
|
1357
1780
|
|
|
1781
|
+
// ---------- 列表检索 / 树内检索 / 时间轴(2026-11-13) ----------
|
|
1782
|
+
|
|
1783
|
+
listQEl.addEventListener('input', () => {
|
|
1784
|
+
state.q = listQEl.value;
|
|
1785
|
+
renderList();
|
|
1786
|
+
});
|
|
1787
|
+
listQEl.addEventListener('keydown', (e) => {
|
|
1788
|
+
if (e.key === 'Escape') {
|
|
1789
|
+
listQEl.value = '';
|
|
1790
|
+
state.q = '';
|
|
1791
|
+
renderList();
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
listAbnEl.addEventListener('click', () => {
|
|
1795
|
+
state.onlyAbnormal = !state.onlyAbnormal;
|
|
1796
|
+
listAbnEl.setAttribute('aria-pressed', state.onlyAbnormal ? 'true' : 'false');
|
|
1797
|
+
renderList();
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* 一个输入框同时作用于两棵树:并排时两栏必须**同一处**高亮 —— 否则「差异」里会混进
|
|
1802
|
+
* 「搜索状态不同」这个人为差异。判定与跳转都在 trace-view(`setQuery` / `nextHit`)。
|
|
1803
|
+
*/
|
|
1804
|
+
function applyTreeQuery() {
|
|
1805
|
+
const n = view.setQuery(treeQEl.value);
|
|
1806
|
+
if (!trace2El.hidden) view2.setQuery(treeQEl.value);
|
|
1807
|
+
treeHitEl.textContent = treeQEl.value.trim() ? `${n} 处` : '';
|
|
1808
|
+
}
|
|
1809
|
+
treeQEl.addEventListener('input', applyTreeQuery);
|
|
1810
|
+
treeQEl.addEventListener('keydown', (e) => {
|
|
1811
|
+
if (e.key === 'Enter') {
|
|
1812
|
+
e.preventDefault();
|
|
1813
|
+
const total = view.setQuery(treeQEl.value);
|
|
1814
|
+
if (total === 0) {
|
|
1815
|
+
treeHitEl.textContent = treeQEl.value.trim() ? '无匹配' : '';
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const i = view.nextHit(e.shiftKey ? -1 : 1);
|
|
1819
|
+
if (!trace2El.hidden) view2.nextHit(e.shiftKey ? -1 : 1);
|
|
1820
|
+
treeHitEl.textContent = `${i}/${total}`;
|
|
1821
|
+
} else if (e.key === 'Escape') {
|
|
1822
|
+
treeQEl.value = '';
|
|
1823
|
+
applyTreeQuery();
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* 时间轴开关:**布局偏好**,与折叠态同一类(进 localStorage,切 run 不重置)。
|
|
1829
|
+
* 存不下就只活这一次会话 —— 不因为记不住而白屏(与 PANE_KEY 同一口径)。
|
|
1830
|
+
*/
|
|
1831
|
+
const TL_KEY = 'agentia.timeline';
|
|
1832
|
+
let timelineOn = (() => {
|
|
1833
|
+
try {
|
|
1834
|
+
return localStorage.getItem(TL_KEY) !== '0';
|
|
1835
|
+
} catch {
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
})();
|
|
1839
|
+
function applyTimeline(on) {
|
|
1840
|
+
timelineOn = on;
|
|
1841
|
+
tlToggleEl.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1842
|
+
view.setTimeline(on);
|
|
1843
|
+
view2.setTimeline(on);
|
|
1844
|
+
}
|
|
1845
|
+
tlToggleEl.addEventListener('click', () => {
|
|
1846
|
+
applyTimeline(!timelineOn);
|
|
1847
|
+
try {
|
|
1848
|
+
localStorage.setItem(TL_KEY, timelineOn ? '1' : '0');
|
|
1849
|
+
} catch {
|
|
1850
|
+
/* 同上:这一次会话内仍然有效 */
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
applyTimeline(timelineOn);
|
|
1854
|
+
|
|
1855
|
+
// `/` 聚焦列表搜索(不在输入框里时)。这是「迅速找回刚才那条」的入口,
|
|
1856
|
+
// 而列表是这个面板唯一有几十条内容的地方。
|
|
1857
|
+
document.addEventListener('keydown', (e) => {
|
|
1858
|
+
if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1859
|
+
// `??`(不是 `||`):tagName 只可能缺省或非空,用 `||` 与 `??` 在这里同义 —— 但 `??`
|
|
1860
|
+
// 不会把将来的空字符串值也替换掉,意图更直白
|
|
1861
|
+
const tag = e.target?.tagName ?? '';
|
|
1862
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
1863
|
+
e.preventDefault();
|
|
1864
|
+
listQEl.focus();
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1358
1867
|
// ---------- 控件接线(只做「取值 → 调 panel-logic → 写回」) ----------
|
|
1359
1868
|
workdirEl.addEventListener('input', () => {
|
|
1360
1869
|
state.workdir = workdirEl.value;
|
|
@@ -1555,7 +2064,8 @@
|
|
|
1555
2064
|
|
|
1556
2065
|
await refreshDev();
|
|
1557
2066
|
await refreshSession();
|
|
1558
|
-
|
|
2067
|
+
// 深链优先(`?run=<id>`):刷新 / 贴链接回到那条,而不是最新那条
|
|
2068
|
+
await openLatest(runIdFromSearch(location.search));
|
|
1559
2069
|
renderChat();
|
|
1560
2070
|
</script>
|
|
1561
2071
|
</body>
|