@migor/cli 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -2
- package/README.md +5 -5
- package/dist/AGENTS.md +116 -8
- package/dist/cli.js +4 -2
- package/dist/create.js +18 -3
- package/dist/dev-protocol.js +33 -0
- package/dist/dev-runner.js +445 -0
- package/dist/dev.js +947 -63
- package/dist/inspector-page.html +687 -34
- package/dist/inspector-sink.js +4 -5
- package/dist/inspector.js +326 -19
- package/dist/panel-logic.js +248 -0
- package/dist/registry.js +10 -3
- package/dist/templates/README.md +18 -2
- package/dist/templates/capabilities/subagent/index.ts +5 -1
- package/dist/templates/capabilities/subagent/system.md +15 -0
- package/dist/templates/gitignore +3 -0
- package/dist/templates/package.json +2 -2
- package/dist/templates/src/app.ts +106 -0
- package/dist/templates/src/dev.config.ts +40 -0
- package/dist/templates/src/main.ts +8 -26
- package/dist/templates/src/registry.ts +3 -0
- package/dist/templates/src/session-store.ts +68 -0
- package/dist/templates/src/tools/read-file/index.ts +70 -0
- package/dist/templates.js +51 -4
- package/package.json +1 -1
- package/dist/inspector-preload.js +0 -31
package/dist/inspector-page.html
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
--border: #27272a;
|
|
15
15
|
--bg: #0b0b0d;
|
|
16
16
|
--card: #111113;
|
|
17
|
+
--pop: #17171a;
|
|
18
|
+
--warn: #e0af68;
|
|
19
|
+
--err: #f7768e;
|
|
17
20
|
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
18
21
|
}
|
|
19
22
|
* { box-sizing: border-box; }
|
|
@@ -31,29 +34,126 @@
|
|
|
31
34
|
header .sub { color: var(--faint); font-size: 12px; }
|
|
32
35
|
header .live { margin-left: auto; color: var(--faint); font-size: 12px; }
|
|
33
36
|
header .live b { color: var(--ice); font-weight: 500; }
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
/* 告警条:dev 环的「能跑但有东西不对劲」全在这里,不静默降级 */
|
|
39
|
+
#banner {
|
|
40
|
+
margin: 0 20px; padding: 9px 12px; border: 1px solid var(--warn); border-radius: 6px;
|
|
41
|
+
background: #1c1a14; color: var(--warn); font-family: var(--mono); font-size: 12px;
|
|
42
|
+
white-space: pre-wrap; word-break: break-word;
|
|
43
|
+
}
|
|
44
|
+
#banner.err { border-color: var(--err); background: #1d1416; color: var(--err); }
|
|
45
|
+
|
|
46
|
+
main {
|
|
47
|
+
display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 14px;
|
|
48
|
+
padding: 14px 20px;
|
|
49
|
+
}
|
|
35
50
|
aside, section {
|
|
36
51
|
background: var(--card); border: 1px solid var(--border); border-radius: 6px;
|
|
37
52
|
}
|
|
38
|
-
aside { overflow: hidden auto; max-height: calc(100vh -
|
|
53
|
+
aside { overflow: hidden auto; max-height: calc(100vh - 200px); }
|
|
39
54
|
section { padding: 14px; min-height: 240px; font-family: var(--mono); font-size: 12px; overflow: auto; }
|
|
40
55
|
.run {
|
|
41
56
|
display: block; width: 100%; text-align: left; cursor: pointer;
|
|
42
57
|
background: none; border: 0; border-bottom: 1px solid var(--border);
|
|
43
58
|
color: var(--text); padding: 10px 12px; font: inherit;
|
|
44
59
|
}
|
|
45
|
-
.run:hover { background:
|
|
46
|
-
.run.active { background:
|
|
60
|
+
.run:hover { background: var(--pop); }
|
|
61
|
+
.run.active { background: var(--pop); box-shadow: inset 2px 0 0 var(--ice); }
|
|
47
62
|
.run .r1 { display: flex; align-items: center; gap: 6px; }
|
|
48
63
|
.run .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ice); flex: none; }
|
|
49
|
-
.run.err .dot { background:
|
|
64
|
+
.run.err .dot { background: var(--err); }
|
|
65
|
+
/* 被中止的 run 不是失败:给它一个中性偏警告的颜色,不跟失败共用红色 ——
|
|
66
|
+
否则用户会把「我自己按了中止」读成「我把工程改坏了」 */
|
|
67
|
+
.run.aborted .dot { background: var(--warn); }
|
|
50
68
|
.run .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
51
|
-
.run .r2 { margin-top: 3px; color: var(--faint); font-size: 11px; font-family: var(--mono); }
|
|
69
|
+
.run .r2, .run .r3 { margin-top: 3px; color: var(--faint); font-size: 11px; font-family: var(--mono); }
|
|
70
|
+
.run .r3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
52
71
|
.empty { color: var(--faint); padding: 16px; }
|
|
53
72
|
#ttl { color: var(--faint); margin-bottom: 10px; font-family: var(--mono); font-size: 11.5px; }
|
|
54
73
|
.usage { display: flex; gap: 16px; margin-top: 12px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
55
74
|
/* biome-ignore lint/style/noDescendingSpecificity: 本行只颜色/字重,对手是 33 行的 header .live b —— 两者祖先容器不同(.usage 在面板区、.live 在 header),结构上不可能命中同一元素,是规则「只比末段标签」的误报。 */
|
|
56
75
|
.usage b { color: var(--text); font-weight: 500; }
|
|
76
|
+
|
|
77
|
+
/* 页签:对话视图按需出现(多轮开 + 有会话文件),不做空壳 */
|
|
78
|
+
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
|
79
|
+
.tab {
|
|
80
|
+
flex: 1; background: none; border: 0; border-bottom: 2px solid transparent;
|
|
81
|
+
color: var(--faint); padding: 9px 6px; font: inherit; font-size: 12px; cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
.tab.active { color: var(--text); border-bottom-color: var(--ice); }
|
|
84
|
+
|
|
85
|
+
/* 对话流 */
|
|
86
|
+
.turn { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
|
87
|
+
.turn .who { color: var(--faint); font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; }
|
|
88
|
+
.turn .msg { margin-top: 3px; white-space: pre-wrap; word-break: break-word; font-family: var(--mono); font-size: 11.5px; }
|
|
89
|
+
.turn.failed { border-left: 2px solid var(--err); }
|
|
90
|
+
.turn.failed .msg { color: var(--err); }
|
|
91
|
+
/* 中止轮:左边线用警告色而不是错误色(它不是失败,只是没跑完) */
|
|
92
|
+
.turn.aborted { border-left: 2px solid var(--warn); }
|
|
93
|
+
.turn.aborted .msg { color: var(--muted); }
|
|
94
|
+
|
|
95
|
+
/* 清空对话条:只在对话视图出现时显示(不做空壳) */
|
|
96
|
+
#chat-bar {
|
|
97
|
+
display: flex; align-items: center; gap: 8px;
|
|
98
|
+
padding: 7px 12px; border-bottom: 1px solid var(--border);
|
|
99
|
+
}
|
|
100
|
+
#chat-bar[hidden] { display: none; }
|
|
101
|
+
#chat-sid { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
|
|
102
|
+
|
|
103
|
+
#reply {
|
|
104
|
+
margin: 14px 0 0; padding: 10px; border: 1px solid var(--border); border-radius: 4px;
|
|
105
|
+
background: #0e0e10; white-space: pre-wrap; word-break: break-word; color: var(--muted);
|
|
106
|
+
}
|
|
107
|
+
#reply[hidden] { display: none; }
|
|
108
|
+
|
|
109
|
+
/* 输入条:固定在底部、横跨两栏、分两行(四个控件挤一行会太窄) */
|
|
110
|
+
footer {
|
|
111
|
+
position: sticky; bottom: 0; margin-top: 4px;
|
|
112
|
+
border-top: 1px solid var(--border); background: var(--bg);
|
|
113
|
+
padding: 10px 20px 14px; display: grid; gap: 8px;
|
|
114
|
+
}
|
|
115
|
+
footer[hidden] { display: none; }
|
|
116
|
+
.row { display: flex; gap: 8px; align-items: center; }
|
|
117
|
+
.ctl {
|
|
118
|
+
background: var(--card); border: 1px solid var(--border); border-radius: 5px;
|
|
119
|
+
color: var(--text); padding: 7px 10px; font: inherit; font-size: 12px;
|
|
120
|
+
}
|
|
121
|
+
.ctl:focus { outline: 1px solid var(--ice); border-color: var(--ice); }
|
|
122
|
+
.ctl:disabled { opacity: .55; cursor: default; }
|
|
123
|
+
/* 中止:唯一带破坏性的按钮,用告警色区分(只在有在飞 run 时出现) */
|
|
124
|
+
#abort { color: var(--err); border-color: var(--err); }
|
|
125
|
+
#abort[hidden] { display: none; }
|
|
126
|
+
#caps-sum { cursor: pointer; list-style: none; user-select: none; font-family: var(--mono); }
|
|
127
|
+
#caps-sum::-webkit-details-marker { display: none; }
|
|
128
|
+
details { position: relative; }
|
|
129
|
+
.pop {
|
|
130
|
+
position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 10;
|
|
131
|
+
min-width: 260px; max-height: 320px; overflow: auto;
|
|
132
|
+
background: var(--pop); border: 1px solid var(--border); border-radius: 6px;
|
|
133
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, .5); padding: 8px;
|
|
134
|
+
}
|
|
135
|
+
.pop[hidden] { display: none; }
|
|
136
|
+
.cap {
|
|
137
|
+
display: flex; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 4px;
|
|
138
|
+
cursor: pointer; font-family: var(--mono); font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
.cap:hover { background: #202024; }
|
|
141
|
+
#workdir { flex: 1; min-width: 0; font-family: var(--mono); }
|
|
142
|
+
#prompt { flex: 1; min-width: 0; }
|
|
143
|
+
#run {
|
|
144
|
+
background: var(--ice); border-color: var(--ice); color: #04222a;
|
|
145
|
+
font-weight: 600; min-width: 88px;
|
|
146
|
+
}
|
|
147
|
+
#run:disabled { opacity: .45; cursor: not-allowed; }
|
|
148
|
+
.mt { display: flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 12px; }
|
|
149
|
+
.hint { color: var(--faint); font-size: 11px; }
|
|
150
|
+
.fs-head { display: flex; align-items: center; gap: 8px; padding: 4px 6px 8px; font-family: var(--mono); font-size: 11px; }
|
|
151
|
+
.fs-head span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); }
|
|
152
|
+
.fs-item { display: block; width: 100%; text-align: left; background: none; border: 0; color: var(--text); padding: 4px 6px; border-radius: 4px; font: inherit; font-family: var(--mono); font-size: 12px; cursor: pointer; }
|
|
153
|
+
.fs-item:hover { background: #202024; }
|
|
154
|
+
.fs-foot { padding-top: 8px; border-top: 1px solid var(--border); margin-top: 6px; }
|
|
155
|
+
.mini { background: var(--card); border: 1px solid var(--border); border-radius: 4px; color: var(--text); padding: 5px 9px; font: inherit; font-size: 11.5px; cursor: pointer; }
|
|
156
|
+
.mini:hover { border-color: var(--ice); }
|
|
57
157
|
</style>
|
|
58
158
|
</head>
|
|
59
159
|
<body>
|
|
@@ -62,13 +162,31 @@
|
|
|
62
162
|
<span class="sub">每次 run 的调用树与能力执行</span>
|
|
63
163
|
<span class="live" id="live">等待 run…</span>
|
|
64
164
|
</header>
|
|
165
|
+
<div id="banner" hidden></div>
|
|
65
166
|
<main>
|
|
66
|
-
<aside
|
|
167
|
+
<aside>
|
|
168
|
+
<div class="tabs" id="tabs" hidden>
|
|
169
|
+
<button class="tab active" id="tab-runs" type="button">run 列表</button>
|
|
170
|
+
<button class="tab" id="tab-chat" type="button">对话</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div id="runs"><div class="empty">还没有 run。触发一次 agent 调用即可。</div></div>
|
|
173
|
+
<!--
|
|
174
|
+
清空对话(§6 待定 3):清空 = **换一个 sessionId**(不动 session.json ——
|
|
175
|
+
那是 store 的账,面板对它只读)。所以旧对话还在盘上、run 列表里也还看得到,
|
|
176
|
+
只是模型不再带着它跑了。只在对话视图出现时才显示(不做空壳)。
|
|
177
|
+
-->
|
|
178
|
+
<div id="chat-bar" hidden>
|
|
179
|
+
<span id="chat-sid" class="hint"></span>
|
|
180
|
+
<button class="mini" id="chat-clear" type="button">清空对话</button>
|
|
181
|
+
</div>
|
|
182
|
+
<div id="chat" hidden></div>
|
|
183
|
+
</aside>
|
|
67
184
|
<section>
|
|
68
185
|
<div id="ttl"></div>
|
|
69
186
|
<div id="trace"><div class="tr-empty">// 选一个 run 查看调用树</div></div>
|
|
70
187
|
<!-- 能力排行(G2):哪个能力慢 / 贵 / 爱失败 —— 调优从这里下手 -->
|
|
71
188
|
<div id="summary"></div>
|
|
189
|
+
<pre id="reply" hidden></pre>
|
|
72
190
|
<div class="usage" id="usage" hidden>
|
|
73
191
|
<span>in <b id="u-in">0</b></span>
|
|
74
192
|
<span>out <b id="u-out">0</b></span>
|
|
@@ -76,22 +194,113 @@
|
|
|
76
194
|
</section>
|
|
77
195
|
</main>
|
|
78
196
|
|
|
197
|
+
<!--
|
|
198
|
+
输入条(D7):固定在底部、横跨两栏、分两行。
|
|
199
|
+
第一行 = 这次 run 的配置(能力多选 / 工作目录 / 多轮开关);第二行 = 主操作(prompt + 运行)。
|
|
200
|
+
只在 dev 环在场时出现(GET /api/dev 的 available)—— 不做空壳。
|
|
201
|
+
-->
|
|
202
|
+
<footer id="bar" hidden>
|
|
203
|
+
<div class="row">
|
|
204
|
+
<details id="caps">
|
|
205
|
+
<summary class="ctl" id="caps-sum">能力</summary>
|
|
206
|
+
<div class="pop" id="caps-pop"></div>
|
|
207
|
+
</details>
|
|
208
|
+
<input id="workdir" class="ctl" type="text" spellcheck="false" placeholder="工作目录(绝对路径)" />
|
|
209
|
+
<button id="browse" class="ctl mini" type="button">浏览…</button>
|
|
210
|
+
<label class="mt ctl" title="多轮的初始值 = 所选能力声明的 OR;可在这里覆盖">
|
|
211
|
+
<input type="checkbox" id="mt" /> <span id="mt-label">单轮</span>
|
|
212
|
+
</label>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="row">
|
|
215
|
+
<input id="prompt" class="ctl" type="text" spellcheck="false" placeholder="输入 prompt,回车运行(↑/↓ 调回历史)" />
|
|
216
|
+
<button id="run" class="ctl" type="button">▶ 运行</button>
|
|
217
|
+
<!--
|
|
218
|
+
中止(§6 待定 5 的 kill 按钮):只在有在飞 run 时出现。
|
|
219
|
+
没有它的话,一个卡住的 run 会让「运行」永远 409 —— 面板锁死。
|
|
220
|
+
-->
|
|
221
|
+
<button id="abort" class="ctl" type="button" hidden>■ 中止</button>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="hint" id="bar-hint"></div>
|
|
224
|
+
<div class="pop" id="fs" hidden>
|
|
225
|
+
<div class="fs-head">
|
|
226
|
+
<button class="mini" id="fs-up" type="button">..</button>
|
|
227
|
+
<span id="fs-path"></span>
|
|
228
|
+
</div>
|
|
229
|
+
<div id="fs-list"></div>
|
|
230
|
+
<div class="fs-foot"><button class="mini" id="fs-use" type="button">选这个目录</button></div>
|
|
231
|
+
</div>
|
|
232
|
+
</footer>
|
|
233
|
+
|
|
79
234
|
<script type="module">
|
|
80
235
|
import { createTraceView, playTrace, summarizeTrace, renderSummary } from './index.js';
|
|
236
|
+
import {
|
|
237
|
+
HISTORY_DRAFT,
|
|
238
|
+
capabilityBadge,
|
|
239
|
+
historyStep,
|
|
240
|
+
historyText,
|
|
241
|
+
mergeConversation,
|
|
242
|
+
multiTurnDefault,
|
|
243
|
+
multiTurnLabel,
|
|
244
|
+
normalizeToolSources,
|
|
245
|
+
pushPromptHistory,
|
|
246
|
+
runDoneNotice,
|
|
247
|
+
runIsFailure,
|
|
248
|
+
shortenPath,
|
|
249
|
+
} from './panel-logic.js';
|
|
250
|
+
|
|
251
|
+
const $ = (id) => document.getElementById(id);
|
|
252
|
+
const runsEl = $('runs');
|
|
253
|
+
const chatEl = $('chat');
|
|
254
|
+
const chatBarEl = $('chat-bar');
|
|
255
|
+
const chatSidEl = $('chat-sid');
|
|
256
|
+
const chatClearEl = $('chat-clear');
|
|
257
|
+
const traceEl = $('trace');
|
|
258
|
+
const summaryEl = $('summary');
|
|
259
|
+
const ttlEl = $('ttl');
|
|
260
|
+
const liveEl = $('live');
|
|
261
|
+
const usageEl = $('usage');
|
|
262
|
+
const replyEl = $('reply');
|
|
263
|
+
const bannerEl = $('banner');
|
|
264
|
+
const barEl = $('bar');
|
|
265
|
+
const tabsEl = $('tabs');
|
|
266
|
+
const capsEl = $('caps');
|
|
267
|
+
const capsSumEl = $('caps-sum');
|
|
268
|
+
const capsPopEl = $('caps-pop');
|
|
269
|
+
const workdirEl = $('workdir');
|
|
270
|
+
const mtEl = $('mt');
|
|
271
|
+
const mtLabelEl = $('mt-label');
|
|
272
|
+
const promptEl = $('prompt');
|
|
273
|
+
const runEl = $('run');
|
|
274
|
+
const abortEl = $('abort');
|
|
275
|
+
const hintEl = $('bar-hint');
|
|
276
|
+
const fsEl = $('fs');
|
|
277
|
+
const fsListEl = $('fs-list');
|
|
278
|
+
const fsPathEl = $('fs-path');
|
|
81
279
|
|
|
82
|
-
const runsEl = document.getElementById('runs');
|
|
83
|
-
const traceEl = document.getElementById('trace');
|
|
84
|
-
const summaryEl = document.getElementById('summary');
|
|
85
|
-
const ttlEl = document.getElementById('ttl');
|
|
86
|
-
const liveEl = document.getElementById('live');
|
|
87
|
-
const usageEl = document.getElementById('usage');
|
|
88
280
|
const view = createTraceView(traceEl, {
|
|
89
281
|
// 无公开单价 → 不估算成本;只展示 token
|
|
90
|
-
usage: { in:
|
|
282
|
+
usage: { in: $('u-in'), out: $('u-out') },
|
|
91
283
|
});
|
|
92
284
|
|
|
93
|
-
|
|
94
|
-
|
|
285
|
+
// ---------- 状态 ----------
|
|
286
|
+
// 只放「渲染要用到的东西」;所有判断都走 panel-logic(那份有单测,这份没有)
|
|
287
|
+
const state = {
|
|
288
|
+
dev: null, // GET /api/dev 的结果;null = 还没拿到
|
|
289
|
+
sessionId: '', // 当前会话 id(来自 dev.sessionId)—— **不写死**:清空对话会换它
|
|
290
|
+
selected: [], // 选中的能力 token
|
|
291
|
+
multiTurn: null, // 用户显式覆盖后的值;null = 跟随默认
|
|
292
|
+
workdir: '',
|
|
293
|
+
history: [],
|
|
294
|
+
cursor: HISTORY_DRAFT,
|
|
295
|
+
draft: '',
|
|
296
|
+
session: null, // { messages } | null
|
|
297
|
+
tab: 'runs',
|
|
298
|
+
list: [],
|
|
299
|
+
current: null,
|
|
300
|
+
reply: '',
|
|
301
|
+
notice: '', // 面板自己发的操作反馈(已接受 / 重启了 runner / 出错)
|
|
302
|
+
noticeErr: false,
|
|
303
|
+
};
|
|
95
304
|
|
|
96
305
|
const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
|
|
97
306
|
const fmtNum = (n) => Number(n || 0).toLocaleString('en-US');
|
|
@@ -99,50 +308,447 @@
|
|
|
99
308
|
const s = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
100
309
|
return s < 60 ? s + 's 前' : Math.round(s / 60) + 'm 前';
|
|
101
310
|
};
|
|
311
|
+
const short = (p) => shortenPath(p, { home: state.dev?.home ?? '', cwd: state.dev?.projectRoot ?? '' });
|
|
102
312
|
|
|
313
|
+
// ---------- 告警条 ----------
|
|
314
|
+
// dev 环的 warning / lastError、以及面板自己的操作反馈,都在这里显示 ——
|
|
315
|
+
// 一律 textContent(内容含用户路径与模型输出)
|
|
316
|
+
function renderBanner() {
|
|
317
|
+
const dev = state.dev;
|
|
318
|
+
const parts = [];
|
|
319
|
+
if (state.notice) parts.push(state.notice);
|
|
320
|
+
if (dev?.warning) parts.push('⚠ ' + dev.warning);
|
|
321
|
+
if (dev?.lastError) parts.push('✖ ' + dev.lastError);
|
|
322
|
+
if (parts.length === 0) {
|
|
323
|
+
bannerEl.hidden = true;
|
|
324
|
+
bannerEl.textContent = '';
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
bannerEl.hidden = false;
|
|
328
|
+
bannerEl.textContent = parts.join('\n');
|
|
329
|
+
bannerEl.className = state.noticeErr ? 'err' : '';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------- 输入条 ----------
|
|
333
|
+
function renderControls() {
|
|
334
|
+
const dev = state.dev;
|
|
335
|
+
if (!dev?.available) {
|
|
336
|
+
barEl.hidden = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
barEl.hidden = false;
|
|
340
|
+
|
|
341
|
+
// 能力多选(默认全选)。全选时 badge 显示 N/N —— 一眼看出这次是全量还是收窄
|
|
342
|
+
const all = dev.capabilities;
|
|
343
|
+
if (state.selected.length === 0 && all.length > 0) state.selected = [...all];
|
|
344
|
+
capsSumEl.textContent = `能力 ${capabilityBadge(state.selected, all)} ▾`;
|
|
345
|
+
capsPopEl.replaceChildren();
|
|
346
|
+
if (all.length === 0) {
|
|
347
|
+
const d = document.createElement('div');
|
|
348
|
+
d.className = 'hint';
|
|
349
|
+
d.textContent = '没发现能力(app.ts 是否导出了 CAPABILITY_DIRS?)';
|
|
350
|
+
capsPopEl.appendChild(d);
|
|
351
|
+
}
|
|
352
|
+
for (const name of all) {
|
|
353
|
+
const label = document.createElement('label');
|
|
354
|
+
label.className = 'cap';
|
|
355
|
+
const box = document.createElement('input');
|
|
356
|
+
box.type = 'checkbox';
|
|
357
|
+
box.checked = state.selected.includes(name);
|
|
358
|
+
box.addEventListener('change', () => {
|
|
359
|
+
const next = box.checked
|
|
360
|
+
? [...state.selected, name]
|
|
361
|
+
: state.selected.filter((x) => x !== name);
|
|
362
|
+
// 不许取消到空:空集与「全量」在语义上打架(normalizeToolSources 会兜成全量,
|
|
363
|
+
// 但那样勾选态与实发参数就不一致了)—— 在 UI 层就挡住
|
|
364
|
+
if (next.length === 0) {
|
|
365
|
+
box.checked = true;
|
|
366
|
+
setNotice('至少留一个能力(全不选与全选同义,直接用全选)', true);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
state.selected = next;
|
|
370
|
+
state.multiTurn = null; // 选择变了 ⇒ 回到默认位(默认值本身跟着选择走)
|
|
371
|
+
renderControls();
|
|
372
|
+
renderBanner();
|
|
373
|
+
});
|
|
374
|
+
const text = document.createElement('span');
|
|
375
|
+
text.textContent = name;
|
|
376
|
+
const star = document.createElement('span');
|
|
377
|
+
star.className = 'hint';
|
|
378
|
+
star.textContent = dev.multiTurn.includes(name) ? '多轮' : '';
|
|
379
|
+
label.append(box, text, star);
|
|
380
|
+
capsPopEl.appendChild(label);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 多轮开关:初始值 = 所选能力声明的 OR,**并把来源标出来**(混选时默认有歧义)
|
|
384
|
+
const def = multiTurnDefault(state.selected, dev.multiTurn);
|
|
385
|
+
const value = state.multiTurn ?? def.value;
|
|
386
|
+
mtEl.checked = value;
|
|
387
|
+
mtLabelEl.textContent = multiTurnLabel(value, def.sources, state.multiTurn === null);
|
|
388
|
+
|
|
389
|
+
// 工作目录:默认值来自 dev.config / 项目根;这里是 per-run 的输入
|
|
390
|
+
if (workdirEl.value === '') workdirEl.value = dev.defaultWorkdir;
|
|
391
|
+
state.workdir = workdirEl.value;
|
|
392
|
+
runEl.disabled = dev.running;
|
|
393
|
+
runEl.textContent = dev.running ? '运行中…' : '▶ 运行';
|
|
394
|
+
// 中止(§6 待定 5 的 kill 按钮):只在有在飞 run 时出现。
|
|
395
|
+
// 没有它,一个卡住的 run 会让「运行」永远 409 —— 面板锁死(这正是它存在的理由)。
|
|
396
|
+
abortEl.hidden = !dev.running;
|
|
397
|
+
abortEl.disabled = !dev.running;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function setNotice(text, isError) {
|
|
401
|
+
state.notice = text;
|
|
402
|
+
state.noticeErr = !!isError;
|
|
403
|
+
renderBanner();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function readControls() {
|
|
407
|
+
const dev = state.dev;
|
|
408
|
+
const all = dev ? dev.capabilities : [];
|
|
409
|
+
return {
|
|
410
|
+
prompt: promptEl.value.trim(),
|
|
411
|
+
workdir: workdirEl.value.trim(),
|
|
412
|
+
toolSources: normalizeToolSources(state.selected, all),
|
|
413
|
+
multiTurn: state.multiTurn ?? multiTurnDefault(state.selected, dev ? dev.multiTurn : []).value,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------- run 列表 ----------
|
|
103
418
|
function renderList() {
|
|
104
|
-
if (!list.length) {
|
|
105
|
-
runsEl.
|
|
419
|
+
if (!state.list.length) {
|
|
420
|
+
runsEl.replaceChildren();
|
|
421
|
+
const d = document.createElement('div');
|
|
422
|
+
d.className = 'empty';
|
|
423
|
+
d.textContent = '还没有 run。触发一次 agent 调用即可。';
|
|
424
|
+
runsEl.appendChild(d);
|
|
106
425
|
return;
|
|
107
426
|
}
|
|
108
|
-
runsEl.
|
|
109
|
-
for (const r of list) {
|
|
427
|
+
runsEl.replaceChildren();
|
|
428
|
+
for (const r of state.list) {
|
|
110
429
|
const b = document.createElement('button');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
b.
|
|
430
|
+
// 三态:失败 / **被中止** / 正常。中止不是失败(判别走 panel-logic 的
|
|
431
|
+
// runIsFailure,与通知条同一条语义)—— 给中止的 run 标红会让用户以为
|
|
432
|
+
// 是自己把工程改坏了,而那正是「aborted 也带 error」这条引擎约定造成的误读。
|
|
433
|
+
const tone = runIsFailure(r) ? ' err' : r.stopReason === 'aborted' ? ' aborted' : '';
|
|
434
|
+
b.className = 'run' + tone + (r.traceId === state.current ? ' active' : '');
|
|
435
|
+
b.type = 'button';
|
|
436
|
+
|
|
437
|
+
const r1 = document.createElement('span');
|
|
438
|
+
r1.className = 'r1';
|
|
439
|
+
const dot = document.createElement('span');
|
|
440
|
+
dot.className = 'dot';
|
|
441
|
+
const name = document.createElement('span');
|
|
442
|
+
name.className = 'name';
|
|
443
|
+
name.textContent = r.name; // 模型可控内容 ⇒ 只能 textContent
|
|
444
|
+
r1.append(dot, name);
|
|
445
|
+
|
|
446
|
+
const r2 = document.createElement('span');
|
|
447
|
+
r2.className = 'r2';
|
|
448
|
+
r2.textContent = `${fmtMs(r.ms)} · ${fmtNum(r.tokens)} tok · ${ago(r.startedAt)}`;
|
|
449
|
+
|
|
450
|
+
b.append(r1, r2);
|
|
451
|
+
|
|
452
|
+
// 第三行 = CLI 侧记账(目标目录 + 能力组合)。不是 dev 起的 run 没有这一行
|
|
453
|
+
if (r.note) {
|
|
454
|
+
const r3 = document.createElement('span');
|
|
455
|
+
r3.className = 'r3';
|
|
456
|
+
const caps = r.note.toolSources === null ? '全量' : r.note.toolSources.join(',');
|
|
457
|
+
r3.textContent = `${short(r.note.workdir)} · ${caps}${r.note.multiTurn ? ' · 多轮' : ''}`;
|
|
458
|
+
b.appendChild(r3);
|
|
459
|
+
}
|
|
116
460
|
b.addEventListener('click', () => open(r.traceId));
|
|
117
461
|
runsEl.appendChild(b);
|
|
118
462
|
}
|
|
119
463
|
}
|
|
120
464
|
|
|
465
|
+
// ---------- 对话流(只在多轮开 + 有会话文件时出现) ----------
|
|
466
|
+
function renderChat() {
|
|
467
|
+
const dev = state.dev;
|
|
468
|
+
const def = multiTurnDefault(state.selected, dev ? dev.multiTurn : []);
|
|
469
|
+
const value = state.multiTurn ?? def.value;
|
|
470
|
+
// 三条都要满足才显示:多轮开着、会话文件读得到、dev 环在场 —— 不做空壳
|
|
471
|
+
const show = !!dev && dev.available && value && state.session !== null;
|
|
472
|
+
tabsEl.hidden = !show;
|
|
473
|
+
// 清空对话条与对话视图同生共死(它清的就是这份对话)——
|
|
474
|
+
// **还要跟页签走**:它长在对话视图上,切回 run 列表时不该留在那儿
|
|
475
|
+
chatBarEl.hidden = !(show && state.tab === 'chat');
|
|
476
|
+
if (!show) {
|
|
477
|
+
chatEl.hidden = true;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
chatSidEl.textContent = `会话 ${state.sessionId}`;
|
|
481
|
+
// 清空会换 id、而 run 在飞时后端会 409(run 结束要写回当前会话)—— 在 UI 层先挡住
|
|
482
|
+
chatClearEl.disabled = dev.running;
|
|
483
|
+
// 会话 id 从 dev 环取,**不写死**:清空对话换的就是它(写死 = 「同一事实两个写法」)
|
|
484
|
+
const turns = mergeConversation(state.session.messages ?? [], state.list, state.sessionId);
|
|
485
|
+
chatEl.replaceChildren();
|
|
486
|
+
if (turns.length === 0) {
|
|
487
|
+
const d = document.createElement('div');
|
|
488
|
+
d.className = 'empty';
|
|
489
|
+
d.textContent = '会话文件还是空的。跑一次多轮 run 就有了。';
|
|
490
|
+
chatEl.appendChild(d);
|
|
491
|
+
}
|
|
492
|
+
for (const t of turns) {
|
|
493
|
+
const box = document.createElement('div');
|
|
494
|
+
box.className = 'turn' + (t.failed ? ' failed' : t.aborted ? ' aborted' : '');
|
|
495
|
+
const who = document.createElement('div');
|
|
496
|
+
who.className = 'who';
|
|
497
|
+
who.textContent = 'user';
|
|
498
|
+
const u = document.createElement('div');
|
|
499
|
+
u.className = 'msg';
|
|
500
|
+
u.textContent = t.user;
|
|
501
|
+
box.append(who, u);
|
|
502
|
+
if (t.assistant !== null) {
|
|
503
|
+
const who2 = document.createElement('div');
|
|
504
|
+
who2.className = 'who';
|
|
505
|
+
who2.textContent = 'assistant';
|
|
506
|
+
const a = document.createElement('div');
|
|
507
|
+
a.className = 'msg';
|
|
508
|
+
a.textContent = t.assistant;
|
|
509
|
+
box.append(who2, a);
|
|
510
|
+
} else if (t.failed) {
|
|
511
|
+
// 失败轮**标失败**,不假装那轮没发生(它没回写会话文件,靠 run 列表 join 出来)
|
|
512
|
+
const f = document.createElement('div');
|
|
513
|
+
f.className = 'msg';
|
|
514
|
+
f.textContent = '(这一轮失败了 —— 会话文件里没有它的回复)';
|
|
515
|
+
box.appendChild(f);
|
|
516
|
+
} else if (t.aborted) {
|
|
517
|
+
// 中止轮同样没回写会话(框架只在成功路径回写)⇒ 也靠 join 出来,
|
|
518
|
+
// 但它不是失败:文案与左边线都要与失败区分开
|
|
519
|
+
const f = document.createElement('div');
|
|
520
|
+
f.className = 'msg';
|
|
521
|
+
f.textContent = '(这一轮被中止了 —— 没有回复写回会话)';
|
|
522
|
+
box.appendChild(f);
|
|
523
|
+
}
|
|
524
|
+
chatEl.appendChild(box);
|
|
525
|
+
}
|
|
526
|
+
chatEl.hidden = state.tab !== 'chat';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function selectTab(tab) {
|
|
530
|
+
state.tab = tab;
|
|
531
|
+
$('tab-runs').className = 'tab' + (tab === 'runs' ? ' active' : '');
|
|
532
|
+
$('tab-chat').className = 'tab' + (tab === 'chat' ? ' active' : '');
|
|
533
|
+
runsEl.hidden = tab !== 'runs';
|
|
534
|
+
chatEl.hidden = tab !== 'chat';
|
|
535
|
+
// 对话视图上的那条「清空对话」也跟着页签显隐 —— 它的可见性在 renderChat 里判
|
|
536
|
+
renderChat();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---------- 拉数据 ----------
|
|
540
|
+
async function refreshDev() {
|
|
541
|
+
try {
|
|
542
|
+
const res = await fetch('/api/dev');
|
|
543
|
+
state.dev = res.ok ? await res.json() : null;
|
|
544
|
+
} catch {
|
|
545
|
+
state.dev = null;
|
|
546
|
+
}
|
|
547
|
+
// 会话 id 由 dev 环持有(清空对话会换它)—— 面板不自己造,只跟着走
|
|
548
|
+
state.sessionId = state.dev?.sessionId ?? '';
|
|
549
|
+
renderControls();
|
|
550
|
+
renderChat();
|
|
551
|
+
renderBanner();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function refreshSession() {
|
|
555
|
+
if (!state.dev?.available) return;
|
|
556
|
+
try {
|
|
557
|
+
const res = await fetch('/api/session');
|
|
558
|
+
const body = res.ok ? await res.json() : { session: null };
|
|
559
|
+
state.session = body.session ?? null;
|
|
560
|
+
} catch {
|
|
561
|
+
state.session = null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
121
565
|
async function refresh() {
|
|
122
|
-
|
|
123
|
-
list
|
|
124
|
-
|
|
566
|
+
// ⚠️ 必须查 `res.ok`:token 过期 / 403 时 body 是 `{ error: … }`,而
|
|
567
|
+
// `renderList` 对非数组只走「`!state.list.length`」那一支 ⇒ 会显示误导性的
|
|
568
|
+
// 空态「还没有 run」。**一个错误的空态比一句错误更难发现**(用户以为只是没跑过)。
|
|
569
|
+
try {
|
|
570
|
+
const res = await fetch('/api/runs');
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
const body = await res.json().catch(() => ({}));
|
|
573
|
+
setNotice(`读 run 列表失败:${body.error || res.status}`, true);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
state.list = await res.json();
|
|
577
|
+
renderList();
|
|
578
|
+
} catch (e) {
|
|
579
|
+
setNotice(`读 run 列表失败:${e.message}`, true);
|
|
580
|
+
}
|
|
125
581
|
}
|
|
126
582
|
|
|
127
583
|
async function open(id) {
|
|
128
584
|
const res = await fetch('/api/runs/' + encodeURIComponent(id));
|
|
129
585
|
if (!res.ok) return;
|
|
130
586
|
const trace = await res.json();
|
|
131
|
-
current = id;
|
|
587
|
+
state.current = id;
|
|
132
588
|
ttlEl.textContent = trace.traceId;
|
|
133
589
|
usageEl.hidden = false;
|
|
134
590
|
playTrace(view, trace);
|
|
135
591
|
summaryEl.innerHTML = renderSummary(summarizeTrace(trace));
|
|
592
|
+
// 回复正文只对**刚跑完的那一轮**有意义(trace 上没有它 —— 它是 run-done 事件带的)
|
|
593
|
+
// ⇒ 打开历史 run 必须清掉,否则它挂在别人的 trace 底下(张冠李戴)
|
|
594
|
+
state.reply = '';
|
|
595
|
+
replyEl.hidden = true;
|
|
596
|
+
replyEl.textContent = '';
|
|
136
597
|
renderList();
|
|
137
598
|
}
|
|
138
599
|
|
|
139
|
-
// 打开最新一条(首屏/新 run 到达时)
|
|
140
600
|
async function openLatest() {
|
|
141
601
|
await refresh();
|
|
142
|
-
if (list.length && !current) await open(list[0].traceId);
|
|
602
|
+
if (state.list.length && !state.current) await open(state.list[0].traceId);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ---------- 目录浏览 ----------
|
|
606
|
+
// 浏览器拿不到用户选的文件夹的绝对路径(webkitdirectory 只给相对路径、
|
|
607
|
+
// File System Access 只给 handle.name),而 dev 环要的是绝对路径 ⇒ 服务端列目录。
|
|
608
|
+
let fsPath = '';
|
|
609
|
+
async function openFs(at) {
|
|
610
|
+
const res = await fetch('/api/fs?path=' + encodeURIComponent(at));
|
|
611
|
+
if (!res.ok) {
|
|
612
|
+
const body = await res.json().catch(() => ({ error: '读目录失败' }));
|
|
613
|
+
setNotice(body.error || '读目录失败', true);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const body = await res.json();
|
|
617
|
+
fsPath = body.path;
|
|
618
|
+
fsPathEl.textContent = body.path;
|
|
619
|
+
fsListEl.replaceChildren();
|
|
620
|
+
for (const name of body.dirs) {
|
|
621
|
+
const b = document.createElement('button');
|
|
622
|
+
b.type = 'button';
|
|
623
|
+
b.className = 'fs-item';
|
|
624
|
+
b.textContent = name + '/';
|
|
625
|
+
b.addEventListener('click', () => openFs(fsPath + '/' + name));
|
|
626
|
+
fsListEl.appendChild(b);
|
|
627
|
+
}
|
|
628
|
+
fsEl.hidden = false;
|
|
629
|
+
fsEl.dataset.parent = body.parent;
|
|
143
630
|
}
|
|
631
|
+
$('fs-up').addEventListener('click', () => openFs(fsEl.dataset.parent || fsPath));
|
|
632
|
+
$('fs-use').addEventListener('click', () => {
|
|
633
|
+
workdirEl.value = fsPath;
|
|
634
|
+
state.workdir = fsPath;
|
|
635
|
+
fsEl.hidden = true;
|
|
636
|
+
setNotice(`工作目录:${short(fsPath)}`, false);
|
|
637
|
+
});
|
|
638
|
+
$('browse').addEventListener('click', () => {
|
|
639
|
+
if (!fsEl.hidden) {
|
|
640
|
+
fsEl.hidden = true;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
void openFs(workdirEl.value.trim() || (state.dev ? state.dev.defaultWorkdir : '.'));
|
|
644
|
+
});
|
|
144
645
|
|
|
145
|
-
//
|
|
646
|
+
// ---------- 控件接线(只做「取值 → 调 panel-logic → 写回」) ----------
|
|
647
|
+
workdirEl.addEventListener('input', () => {
|
|
648
|
+
state.workdir = workdirEl.value;
|
|
649
|
+
});
|
|
650
|
+
mtEl.addEventListener('change', () => {
|
|
651
|
+
state.multiTurn = mtEl.checked; // 显式覆盖(调试就是要试开/关的区别)
|
|
652
|
+
renderControls();
|
|
653
|
+
renderChat();
|
|
654
|
+
});
|
|
655
|
+
document.addEventListener('click', (e) => {
|
|
656
|
+
if (capsEl.open && !capsEl.contains(e.target)) capsEl.open = false;
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
promptEl.addEventListener('keydown', (e) => {
|
|
660
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
661
|
+
// (乙) prompt 回显:只把文字填回输入框,模型看不见 —— 与多轮开关无关
|
|
662
|
+
if (state.cursor === HISTORY_DRAFT) state.draft = promptEl.value;
|
|
663
|
+
state.cursor = historyStep(state.history, state.cursor, e.key === 'ArrowUp' ? 'older' : 'newer');
|
|
664
|
+
promptEl.value = historyText(state.history, state.cursor, state.draft);
|
|
665
|
+
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (e.key === 'Enter') submit();
|
|
670
|
+
});
|
|
671
|
+
runEl.addEventListener('click', submit);
|
|
672
|
+
abortEl.addEventListener('click', abortRun);
|
|
673
|
+
chatClearEl.addEventListener('click', clearSession);
|
|
674
|
+
|
|
675
|
+
async function submit() {
|
|
676
|
+
const req = readControls();
|
|
677
|
+
if (req.prompt.length === 0) {
|
|
678
|
+
setNotice('prompt 不能为空', true);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
state.history = pushPromptHistory(state.history, req.prompt);
|
|
682
|
+
state.cursor = HISTORY_DRAFT;
|
|
683
|
+
state.draft = '';
|
|
684
|
+
// 先禁用(防连点),但**不自己恢复** —— 恢复交给 refreshDev():
|
|
685
|
+
// 「能不能再跑」的真值只在 dev.running 一处,面板自己存一份就会漂
|
|
686
|
+
runEl.disabled = true;
|
|
687
|
+
try {
|
|
688
|
+
const res = await fetch('/run', {
|
|
689
|
+
method: 'POST',
|
|
690
|
+
headers: { 'content-type': 'application/json' },
|
|
691
|
+
// 刻意**不**带 sessionId:它由 dev 环(父进程)自己决定,面板只是视图。
|
|
692
|
+
// 带上它反而危险 —— 一个停留在「清空对话」之前的页面能把新 run 写回**旧**会话
|
|
693
|
+
// (刚清掉的对话自己回来)。理由见 dev-protocol 的 `DevMessage`。
|
|
694
|
+
body: JSON.stringify(req),
|
|
695
|
+
});
|
|
696
|
+
const body = await res.json().catch(() => ({}));
|
|
697
|
+
if (!res.ok) {
|
|
698
|
+
setNotice(`没跑起来:${body.error || res.status}`, true);
|
|
699
|
+
await refreshDev();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
setNotice(body.restarted ? '已接受(换能力选择 ⇒ 重启了 runner 进程)' : '已接受', false);
|
|
703
|
+
await refreshDev();
|
|
704
|
+
} catch (e) {
|
|
705
|
+
setNotice(`请求失败:${e.message}`, true);
|
|
706
|
+
await refreshDev();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ---------- 中止 / 清空对话 ----------
|
|
711
|
+
// 两个都不是「面板自己说了算」的操作:真值在后端(running / sessionId),
|
|
712
|
+
// 面板发完请求一律 refreshDev() 回读,不本地改状态。
|
|
713
|
+
|
|
714
|
+
async function abortRun() {
|
|
715
|
+
abortEl.disabled = true;
|
|
716
|
+
try {
|
|
717
|
+
const res = await fetch('/run/abort', { method: 'POST' });
|
|
718
|
+
const body = await res.json().catch(() => ({}));
|
|
719
|
+
if (!res.ok) {
|
|
720
|
+
setNotice(`中止失败:${body.error || res.status}`, true);
|
|
721
|
+
} else if (body.escalated) {
|
|
722
|
+
setNotice('中止超时(工具不响应 signal)⇒ 已重启 runner 兜底。这次 run 的 trace 丢了', true);
|
|
723
|
+
} else {
|
|
724
|
+
setNotice('已发出中止:run 会以 aborted 收尾,trace 保得住', false);
|
|
725
|
+
}
|
|
726
|
+
} catch (e) {
|
|
727
|
+
setNotice(`中止请求失败:${e.message}`, true);
|
|
728
|
+
}
|
|
729
|
+
await refreshDev();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function clearSession() {
|
|
733
|
+
chatClearEl.disabled = true;
|
|
734
|
+
try {
|
|
735
|
+
const res = await fetch('/session/clear', { method: 'POST' });
|
|
736
|
+
const body = await res.json().catch(() => ({}));
|
|
737
|
+
if (!res.ok) {
|
|
738
|
+
setNotice(`清空失败:${body.error || res.status}`, true);
|
|
739
|
+
} else {
|
|
740
|
+
setNotice(`已清空:新会话 ${body.sessionId}(旧对话还在盘上,模型不再带着它跑)`, false);
|
|
741
|
+
}
|
|
742
|
+
} catch (e) {
|
|
743
|
+
setNotice(`清空请求失败:${e.message}`, true);
|
|
744
|
+
}
|
|
745
|
+
await refreshDev();
|
|
746
|
+
await refreshSession();
|
|
747
|
+
renderChat();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ---------- SSE ----------
|
|
751
|
+
// 两个事件源:默认事件 = run 摘要(老行为);命名事件 `dev` = dev 环的生命周期
|
|
146
752
|
const es = new EventSource('/stream');
|
|
147
753
|
es.onmessage = async (m) => {
|
|
148
754
|
const s = JSON.parse(m.data);
|
|
@@ -153,11 +759,58 @@
|
|
|
153
759
|
nameEl.textContent = s.name;
|
|
154
760
|
liveEl.append(nameEl, ` · ${fmtMs(s.ms)}`);
|
|
155
761
|
await refresh();
|
|
762
|
+
await refreshSession();
|
|
763
|
+
renderChat();
|
|
156
764
|
await open(s.traceId);
|
|
157
765
|
};
|
|
158
|
-
es.
|
|
766
|
+
es.addEventListener('dev', async (m) => {
|
|
767
|
+
const ev = JSON.parse(m.data);
|
|
768
|
+
if (ev.kind === 'run-start') {
|
|
769
|
+
liveEl.textContent = '正在跑…';
|
|
770
|
+
setNotice(
|
|
771
|
+
`运行中:${short(ev.workdir)} · ${ev.toolSources === null ? '全量' : ev.toolSources.join(',')}` +
|
|
772
|
+
`${ev.multiTurn ? ' · 多轮' : ''}`,
|
|
773
|
+
false,
|
|
774
|
+
);
|
|
775
|
+
// run-start 是「确实开跑了」的权威信号 ⇒ 中止按钮在这里出现(不看面板自己的乐观状态)
|
|
776
|
+
await refreshDev();
|
|
777
|
+
} else if (ev.kind === 'run-done') {
|
|
778
|
+
state.reply = ev.finalText || '';
|
|
779
|
+
replyEl.hidden = state.reply.length === 0;
|
|
780
|
+
replyEl.textContent = state.reply;
|
|
781
|
+
// 判别顺序(先 stopReason 后 ok)是**语义**,抽在 panel-logic 里并带单测 ——
|
|
782
|
+
// 中止的 run `ok` 也是 false(引擎刻意给 aborted 带结构化 error),
|
|
783
|
+
// 顺序反了就会把「我按了中止」显示成「run 失败」。
|
|
784
|
+
const n = runDoneNotice(ev);
|
|
785
|
+
setNotice(n.text, n.isError);
|
|
786
|
+
await refreshDev();
|
|
787
|
+
// 多轮 run 会回写会话文件 —— 这里补刷一次,别等默认事件的时序
|
|
788
|
+
await refreshSession();
|
|
789
|
+
renderChat();
|
|
790
|
+
} else if (ev.kind === 'runner-restart') {
|
|
791
|
+
setNotice(`重启 runner:${ev.reason}`, false);
|
|
792
|
+
// 重启期间菜单是过期的(旧 runner 的能力)—— 先刷一次,等 runner-ready 再刷一次。
|
|
793
|
+
await refreshDev();
|
|
794
|
+
} else if (ev.kind === 'runner-ready') {
|
|
795
|
+
// 冷启动时首次 refreshDev() 早于 runner 装配完成 ⇒ 拿到空菜单。
|
|
796
|
+
// 这条事件就是补那一次的(否则菜单空着且无解释,要等用户先跑一次才填上)。
|
|
797
|
+
await refreshDev();
|
|
798
|
+
} else if (ev.kind === 'runner-error') {
|
|
799
|
+
setNotice(ev.message, true);
|
|
800
|
+
await refreshDev();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
es.onerror = () => {
|
|
804
|
+
liveEl.textContent = '连接断开(dev 进程还在吗?)';
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
$('tab-runs').addEventListener('click', () => selectTab('runs'));
|
|
808
|
+
$('tab-chat').addEventListener('click', () => selectTab('chat'));
|
|
159
809
|
|
|
160
|
-
|
|
810
|
+
await refreshDev();
|
|
811
|
+
await refreshSession();
|
|
812
|
+
await openLatest();
|
|
813
|
+
renderChat();
|
|
161
814
|
</script>
|
|
162
815
|
</body>
|
|
163
816
|
</html>
|