dsh-any-agent 0.6.1 → 0.6.3
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 +96 -1
- package/README.md +47 -6
- package/README.zh.md +48 -7
- package/SECURITY.md +46 -0
- package/USAGE-NOTES.en.md +6 -2
- package/USAGE-NOTES.md +10 -2
- package/bin/dsh-any-agent.mjs +2 -1
- package/docs/en/architecture.md +12 -0
- package/docs/en/limits.md +6 -0
- package/docs/en/live-view-design.md +177 -0
- package/docs/en/reference.md +1 -0
- package/docs/en/release.md +21 -14
- package/docs/en/tests.md +6 -5
- package/docs/en/usage.md +3 -1
- package/docs/zh/architecture.md +12 -0
- package/docs/zh/limits.md +6 -0
- package/docs/zh/live-view-design.md +12 -6
- package/docs/zh/reference.md +1 -0
- package/docs/zh/release.md +20 -11
- package/docs/zh/tests.md +6 -5
- package/docs/zh/usage.md +3 -1
- package/package.json +6 -5
- package/src/conversation.mjs +69 -1
- package/src/doctor.mjs +63 -2
- package/src/live-cli.mjs +86 -22
- package/src/live-dashboard.html +477 -53
- package/src/live-view.mjs +281 -24
- package/DESIGN.md +0 -484
- package/docs/brand/README.md +0 -58
- package/docs/brand/header.jpg +0 -0
- package/scripts/check-release.mjs +0 -73
- package/scripts/commit.mjs +0 -122
- package/scripts/detect-dsh.mjs +0 -55
- package/scripts/lint.mjs +0 -595
- package/scripts/sync-test-counts.mjs +0 -699
package/src/live-dashboard.html
CHANGED
|
@@ -54,7 +54,8 @@ button { font: inherit; color: inherit; }
|
|
|
54
54
|
.stats { display: flex; align-items: stretch; height: 100%; margin-left: 4px; }
|
|
55
55
|
.stat { display: flex; flex-direction: column; justify-content: center; padding: 0 16px; border-left: 1px solid var(--line); min-width: 74px; }
|
|
56
56
|
.stat b { font-size: 15px; font-weight: 650; line-height: 1.1; }
|
|
57
|
-
.stat span { font-size: 11px; color: var(--text-3); }
|
|
57
|
+
.stat span { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
58
|
+
.stat { min-width: 0; }
|
|
58
59
|
.stat.hot b { color: var(--run); }
|
|
59
60
|
.stat.warn b { color: var(--warn); }
|
|
60
61
|
.stat.zero b { color: var(--text-3); font-weight: 500; }
|
|
@@ -63,6 +64,9 @@ button { font: inherit; color: inherit; }
|
|
|
63
64
|
.search input { font: inherit; font-size: 12.5px; height: 30px; width: 240px; padding: 0 10px 0 28px; border: 1px solid var(--line); border-radius: 7px; background: var(--sunken); color: var(--text); outline: none; }
|
|
64
65
|
.search input:focus { border-color: var(--accent); background: var(--surface); }
|
|
65
66
|
.search svg { position: absolute; left: 9px; top: 9px; color: var(--text-3); }
|
|
67
|
+
.lang-btn { height: 30px; min-width: 38px; padding: 0 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--sunken); color: var(--text-2); font-size: 12px; font-weight: 600; cursor: pointer; }
|
|
68
|
+
.lang-btn:hover { color: var(--text); border-color: var(--line-2); }
|
|
69
|
+
.lang-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
66
70
|
|
|
67
71
|
/* ── 布局 ───────────────────────────────── */
|
|
68
72
|
.layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); height: calc(100% - 52px); }
|
|
@@ -92,7 +96,66 @@ button { font: inherit; color: inherit; }
|
|
|
92
96
|
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
|
|
93
97
|
|
|
94
98
|
/* ── 主区 ───────────────────────────────── */
|
|
95
|
-
|
|
99
|
+
/* 不再限制最大宽度:超宽屏上调用链和右栏一起铺满,右栏随窗口在 300–460px 之间伸缩。 */
|
|
100
|
+
.wrap { padding: 22px 28px 48px; }
|
|
101
|
+
.cols { display: grid; grid-template-columns: minmax(0, 1fr) clamp(300px, 24vw, 460px); gap: 24px; align-items: start; }
|
|
102
|
+
.inspector { position: sticky; top: 22px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; min-width: 0; }
|
|
103
|
+
.panel { min-width: 0; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 12px 14px; }
|
|
104
|
+
.panel h3 { margin: 0 0 10px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .6px; color: var(--text-3); }
|
|
105
|
+
.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 8px; }
|
|
106
|
+
.kpi b { display: block; font-size: 17px; font-weight: 650; line-height: 1.15; }
|
|
107
|
+
.kpi span { font-size: 11px; color: var(--text-3); }
|
|
108
|
+
.kpi.bad b { color: var(--bad); }
|
|
109
|
+
.kpi.warn b { color: var(--warn); }
|
|
110
|
+
.kpi.zero b { color: var(--text-3); font-weight: 500; }
|
|
111
|
+
.dist { display: grid; gap: 7px; }
|
|
112
|
+
.dist-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px; font-size: 12px; }
|
|
113
|
+
.dist-row .n { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
114
|
+
.dist-row .v { color: var(--text-3); white-space: nowrap; }
|
|
115
|
+
.dist-row .bar { grid-column: 1 / 3; height: 5px; border-radius: 3px; background: var(--sunken); overflow: hidden; }
|
|
116
|
+
.dist-row .bar b { display: block; height: 100%; border-radius: 3px; background: var(--bar-run); opacity: .75; }
|
|
117
|
+
.files { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
|
|
118
|
+
.files li { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 6px; align-items: baseline; font-size: 12px; }
|
|
119
|
+
.files .op { font-size: 10.5px; font-weight: 600; text-align: center; border-radius: 4px; padding: 0 3px; background: var(--sunken); color: var(--text-2); }
|
|
120
|
+
.files .op.w { background: var(--ok-soft); color: var(--ok); }
|
|
121
|
+
.files .p { font-family: var(--mono); font-size: 11.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
|
|
122
|
+
.aps, .dist, .files { grid-template-columns: minmax(0, 1fr); }
|
|
123
|
+
.aps { display: grid; gap: 8px; }
|
|
124
|
+
.ap { min-width: 0; font-size: 12px; border-left: 2px solid var(--warn); padding: 2px 0 2px 8px; }
|
|
125
|
+
.ap .m { font-weight: 600; color: var(--warn); }
|
|
126
|
+
.ap .c { font-family: var(--mono); font-size: 11.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
127
|
+
.muted { font-size: 12px; color: var(--text-3); }
|
|
128
|
+
/* Token 面板 */
|
|
129
|
+
.tok-big { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-bottom: 12px; }
|
|
130
|
+
.tok-big b { display: block; font-size: 18px; font-weight: 650; line-height: 1.15; }
|
|
131
|
+
.tok-big span { font-size: 11px; color: var(--text-3); }
|
|
132
|
+
.stack { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--sunken); margin-bottom: 6px; }
|
|
133
|
+
.stack i { display: block; height: 100%; min-width: 2px; }
|
|
134
|
+
.c-unc { background: var(--accent); }
|
|
135
|
+
.c-read { background: var(--ok); opacity: .75; }
|
|
136
|
+
.c-write { background: var(--warn); opacity: .8; }
|
|
137
|
+
.c-out { background: var(--bad); opacity: .7; }
|
|
138
|
+
.legend { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 10px; font-size: 11.5px; color: var(--text-2); margin-bottom: 12px; }
|
|
139
|
+
.legend span { display: flex; align-items: center; gap: 6px; min-width: 0; white-space: nowrap; }
|
|
140
|
+
.legend i { width: 8px; height: 8px; border-radius: 2px; flex: none; }
|
|
141
|
+
.legend em { font-style: normal; margin-left: auto; color: var(--text-3); }
|
|
142
|
+
.ctx-h { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--text-2); margin-bottom: 4px; }
|
|
143
|
+
.ctx-h span:last-child { color: var(--text-3); }
|
|
144
|
+
.ctx { height: 6px; border-radius: 3px; background: var(--sunken); overflow: hidden; margin-bottom: 12px; }
|
|
145
|
+
.ctx b { display: block; height: 100%; border-radius: 3px; background: var(--accent); }
|
|
146
|
+
.ctx.hi b { background: var(--warn); }
|
|
147
|
+
.ctx.crit b { background: var(--bad); }
|
|
148
|
+
.spark-h { font-size: 11.5px; color: var(--text-2); margin-bottom: 6px; display: flex; justify-content: space-between; }
|
|
149
|
+
.spark-h span:last-child { color: var(--text-3); }
|
|
150
|
+
.spark { display: flex; align-items: flex-end; gap: 3px; height: 56px; }
|
|
151
|
+
.spark i { flex: 1; min-width: 4px; max-width: 26px; border-radius: 3px 3px 0 0; background: var(--bar); position: relative; }
|
|
152
|
+
.spark i.cur { background: var(--bar-run); }
|
|
153
|
+
.spark i:hover { background: var(--accent); }
|
|
154
|
+
.tok-run { font-family: var(--mono); font-size: 11.5px; }
|
|
155
|
+
@media (max-width: 1180px) {
|
|
156
|
+
.cols { grid-template-columns: minmax(0, 1fr); }
|
|
157
|
+
.inspector { position: static; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
|
158
|
+
}
|
|
96
159
|
.hd h1 { font-size: 19px; font-weight: 650; margin: 0 0 10px; letter-spacing: -.1px; word-break: break-word; }
|
|
97
160
|
.facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px 24px; padding: 12px 16px; margin: 0 0 24px; border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); }
|
|
98
161
|
.fact { min-width: 0; }
|
|
@@ -135,7 +198,17 @@ button { font: inherit; color: inherit; }
|
|
|
135
198
|
.collapsed .body { display: none; }
|
|
136
199
|
|
|
137
200
|
/* ── 调用链:工具 / 审批(带时间条) ─────── */
|
|
138
|
-
.spans { border-top: 1px solid var(--line); }
|
|
201
|
+
.spans-box { position: relative; border-top: 1px solid var(--line); }
|
|
202
|
+
/* 工具列表过长时在卡片内部滚动,滚动条隐藏;上下边缘用渐隐提示还有内容。 */
|
|
203
|
+
.spans { max-height: min(60vh, 520px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; -ms-overflow-style: none; }
|
|
204
|
+
.spans::-webkit-scrollbar, .rail::-webkit-scrollbar, .main::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
|
205
|
+
.rail, .main { scrollbar-width: none; -ms-overflow-style: none; }
|
|
206
|
+
.spans-box::before, .spans-box::after { content: ""; position: absolute; left: 0; right: 0; height: 28px; pointer-events: none; opacity: 0; transition: opacity .15s ease; z-index: 1; }
|
|
207
|
+
.spans-box::before { top: 0; background: linear-gradient(var(--surface), transparent); }
|
|
208
|
+
.spans-box::after { bottom: 0; background: linear-gradient(transparent, var(--surface)); border-radius: 0 0 var(--r) var(--r); }
|
|
209
|
+
.spans-box.more-up::before, .spans-box.more-down::after { opacity: 1; }
|
|
210
|
+
.spans-count { position: absolute; right: 12px; bottom: 6px; z-index: 2; font-size: 11px; color: var(--text-3); background: var(--surface); border: 1px solid var(--line); border-radius: 999px; padding: 0 8px; opacity: 0; transition: opacity .15s ease; pointer-events: none; }
|
|
211
|
+
.spans-box.more-down .spans-count { opacity: 1; }
|
|
139
212
|
.span { display: grid; grid-template-columns: 22px 92px minmax(0, 1fr) minmax(120px, 26%) 62px; align-items: center; gap: 10px; padding: 0 12px; height: 32px; border-bottom: 1px solid var(--line); font-size: 12.5px; }
|
|
140
213
|
.span:last-child { border-bottom: 0; }
|
|
141
214
|
.span:hover { background: var(--sunken); }
|
|
@@ -175,7 +248,7 @@ button { font: inherit; color: inherit; }
|
|
|
175
248
|
/* ── 窄屏 ───────────────────────────────── */
|
|
176
249
|
@media (max-width: 820px) {
|
|
177
250
|
.top { height: auto; flex-wrap: wrap; gap: 10px 16px; padding: 10px 16px; position: static; }
|
|
178
|
-
.stats { height: 40px; margin-left: 0; order: 3; width: 100%; }
|
|
251
|
+
.stats { height: 40px; margin-left: 0; order: 3; width: 100%; overflow: hidden; }
|
|
179
252
|
.stat { flex: 1; min-width: 0; padding: 0 10px; }
|
|
180
253
|
.stat span { white-space: nowrap; }
|
|
181
254
|
.stat:first-child { border-left: 0; padding-left: 0; }
|
|
@@ -198,16 +271,17 @@ button { font: inherit; color: inherit; }
|
|
|
198
271
|
</head>
|
|
199
272
|
<body>
|
|
200
273
|
<header class="top">
|
|
201
|
-
<div class="brand"><span class="brand-mark">DS</span>DSH Live <span class="conn" id="conn"><i></i><span
|
|
274
|
+
<div class="brand"><span class="brand-mark">DS</span>DSH Live <span class="conn" id="conn"><i></i><span id="conn-txt">…</span></span></div>
|
|
202
275
|
<div class="stats" id="stats"></div>
|
|
203
276
|
<div class="grow"></div>
|
|
204
277
|
<label class="search">
|
|
205
278
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="7" cy="7" r="5"/><path d="M11 11l4 4"/></svg>
|
|
206
|
-
<input type="search" id="q"
|
|
279
|
+
<input type="search" id="q">
|
|
207
280
|
</label>
|
|
281
|
+
<button type="button" class="lang-btn" id="lang-btn"></button>
|
|
208
282
|
</header>
|
|
209
283
|
<div class="layout">
|
|
210
|
-
<nav class="rail" id="rail"
|
|
284
|
+
<nav class="rail" id="rail"></nav>
|
|
211
285
|
<main class="main" id="main" aria-live="polite"></main>
|
|
212
286
|
</div>
|
|
213
287
|
<script>
|
|
@@ -223,6 +297,110 @@ button { font: inherit; color: inherit; }
|
|
|
223
297
|
let selected = store.get('dsh-live-sel');
|
|
224
298
|
const collapsed = new Set();
|
|
225
299
|
let q = '';
|
|
300
|
+
let connected = null;
|
|
301
|
+
|
|
302
|
+
// ── 界面语言:?lang= > 上次的手动选择 > 浏览器语言 ──
|
|
303
|
+
const I18N = {
|
|
304
|
+
zh: {
|
|
305
|
+
htmlLang: 'zh-CN', switchTo: 'EN', switchLabel: 'Switch to English',
|
|
306
|
+
connecting: '连接中', live: '实时', reconnecting: '重连中',
|
|
307
|
+
search: '筛选标题、id、目录', searchLabel: '筛选会话', railLabel: '会话',
|
|
308
|
+
justNow: '刚刚', minAgo: (n) => n + ' 分钟前', hrAgo: (n) => n + ' 小时前', dayAgo: (n) => n + ' 天前',
|
|
309
|
+
STATE: { running: '运行中', queued: '排队中', idle: '空闲', closed: '已释放' },
|
|
310
|
+
RUN: { running: '运行中', queued: '排队', done: '完成', error: '出错', cancelled: '已取消', interrupted: '中断' },
|
|
311
|
+
VIA: { send: '派活', queue: '排队', interject: '插话' },
|
|
312
|
+
TOOL: { pending: '等待', in_progress: '进行中', unfinished: '未完成', completed: '完成', failed: '失败' },
|
|
313
|
+
MODE: { 'auto-allow': '自动放行', 'auto-deny': '自动拒绝', 'read-only-deny': '只读拒绝', ask: '待人工裁决' },
|
|
314
|
+
sConversations: '会话', sRunning: '运行中', sQueued: '排队', sPending: '待审批', sEscalations: '提权',
|
|
315
|
+
tokAllTitle: (i, cr, o) => '全部会话 token:输入 ' + i + '(缓存读 ' + cr + ')· 输出 ' + o,
|
|
316
|
+
tokStat: (o) => 'token · 出 ' + o,
|
|
317
|
+
unknownClient: '未知客户端', exited: '已退出', untitledShort: (id) => '无标题 · ' + id,
|
|
318
|
+
pendN: (n) => n + ' 待审批', noMatch: '没有匹配的会话', noEvents: '还没有事件',
|
|
319
|
+
noEventsHint: ['用 ', ' 派一次活,调用链会出现在这里。'],
|
|
320
|
+
approval: '审批', waiting: '等待', interrupted: '中断', interject: '插话',
|
|
321
|
+
trackTitle: (a, b) => a + ' 起 · ' + b,
|
|
322
|
+
waitingFirst: '等待第一条事件',
|
|
323
|
+
firstHint: ['这是只读视图。服务端写入 ', ' 事件流后,这里按 客户端 → 会话 → 回合 → 工具 展开调用链。'],
|
|
324
|
+
untitledConv: '无标题会话',
|
|
325
|
+
fState: '状态', fClient: '客户端', unknown: '未知', exitedParen: '(已退出)', fPermission: '权限', fModel: '模型', fConvId: '会话 id', fDir: '目录',
|
|
326
|
+
chain: '调用链', runsCount: (n) => n + ' 个回合 · 最新在上', noRuns: '这个会话还没有派过活',
|
|
327
|
+
turnN: (n) => '第 ' + n + ' 轮 · ', escN: (n) => '⚠ ' + n + ' 次提权', toolsSuffix: () => ' 个工具',
|
|
328
|
+
tokRunTitle: (i, cr, o) => '本回合 token:输入 ' + i + '(缓存读 ' + cr + ')· 输出 ' + o,
|
|
329
|
+
toolList: '工具调用列表',
|
|
330
|
+
foot: '只读视图 · 数据来自本机 live 事件流 · 思考内容从不写入 · 点击回合标题折叠',
|
|
331
|
+
tokTitle: 'Token 消耗', tokNoData: '暂无数据:派一次活之后,这里会实时显示本会话的 token 用量。',
|
|
332
|
+
tIn: '输入', tOut: '输出', tHit: '缓存命中', tUncached: '未缓存输入', tCacheRead: '缓存读', tCacheWrite: '缓存写',
|
|
333
|
+
ctxWindow: '上下文窗口', pctParen: (p) => '(' + p + '%)', perRun: '每回合用量',
|
|
334
|
+
lastN: (n, max) => '最近 ' + n + ' 轮 · 最高 ' + max, barTail: (i, o) => ':输入 ' + i + ' · 输出 ' + o,
|
|
335
|
+
tokFoot: '数字来自 DSH 自己的会话记录(累计,含 live 之前的回合)。',
|
|
336
|
+
overview: '概览', kRuns: '回合', kToolCalls: '工具调用', kFailed: '失败', kToolTime: '工具耗时', kApprovals: '审批', kEscalations: '提权',
|
|
337
|
+
toolDist: '工具分布', noTools: '还没有工具调用', failN: (n) => ' · ' + n + ' 失败',
|
|
338
|
+
files: '涉及文件', noFiles: '还没有读写文件', opW: '写', opR: '读', approvalsTitle: '审批',
|
|
339
|
+
moreBelow: (n) => '↓ ' + n + ' 条',
|
|
340
|
+
},
|
|
341
|
+
en: {
|
|
342
|
+
htmlLang: 'en', switchTo: '中', switchLabel: '切换到中文',
|
|
343
|
+
connecting: 'connecting', live: 'live', reconnecting: 'reconnecting',
|
|
344
|
+
search: 'Filter by title, id, directory', searchLabel: 'Filter conversations', railLabel: 'Conversations',
|
|
345
|
+
justNow: 'just now', minAgo: (n) => n + ' min ago', hrAgo: (n) => n + ' h ago', dayAgo: (n) => n + ' d ago',
|
|
346
|
+
STATE: { running: 'running', queued: 'queued', idle: 'idle', closed: 'released' },
|
|
347
|
+
RUN: { running: 'running', queued: 'queued', done: 'done', error: 'error', cancelled: 'cancelled', interrupted: 'interrupted' },
|
|
348
|
+
VIA: { send: 'send', queue: 'queue', interject: 'interject' },
|
|
349
|
+
TOOL: { pending: 'pending', in_progress: 'in progress', unfinished: 'unfinished', completed: 'done', failed: 'failed' },
|
|
350
|
+
MODE: { 'auto-allow': 'auto-allowed', 'auto-deny': 'auto-denied', 'read-only-deny': 'denied (read-only)', ask: 'awaiting decision' },
|
|
351
|
+
sConversations: 'conversations', sRunning: 'running', sQueued: 'queued', sPending: 'awaiting approval', sEscalations: 'escalations',
|
|
352
|
+
tokAllTitle: (i, cr, o) => 'All conversations: input ' + i + ' (cache read ' + cr + ') · output ' + o,
|
|
353
|
+
tokStat: (o) => 'tokens · out ' + o,
|
|
354
|
+
unknownClient: 'unknown client', exited: 'exited', untitledShort: (id) => 'untitled · ' + id,
|
|
355
|
+
pendN: (n) => n + ' awaiting approval', noMatch: 'No matching conversations', noEvents: 'No events yet',
|
|
356
|
+
noEventsHint: ['Dispatch something with ', ' and its call chain shows up here.'],
|
|
357
|
+
approval: 'approval', waiting: 'waiting', interrupted: 'interrupted', interject: 'interject',
|
|
358
|
+
trackTitle: (a, b) => 'starts at ' + a + ' · ' + b,
|
|
359
|
+
waitingFirst: 'Waiting for the first event',
|
|
360
|
+
firstHint: ['Read-only view. Once the server writes the ', ' event stream, the call chain appears here: client → conversation → run → tools.'],
|
|
361
|
+
untitledConv: 'Untitled conversation',
|
|
362
|
+
fState: 'State', fClient: 'Client', unknown: 'unknown', exitedParen: ' (exited)', fPermission: 'Permission', fModel: 'Model', fConvId: 'Conversation id', fDir: 'Directory',
|
|
363
|
+
chain: 'Call chain', runsCount: (n) => n + (n === 1 ? ' run' : ' runs') + ' · newest first', noRuns: 'Nothing dispatched in this conversation yet',
|
|
364
|
+
turnN: (n) => 'turn ' + n + ' · ', escN: (n) => '⚠ ' + n + (n === 1 ? ' escalation' : ' escalations'), toolsSuffix: (n) => (n === 1 ? ' tool' : ' tools'),
|
|
365
|
+
tokRunTitle: (i, cr, o) => 'This run: input ' + i + ' (cache read ' + cr + ') · output ' + o,
|
|
366
|
+
toolList: 'Tool calls',
|
|
367
|
+
foot: 'Read-only · data from the local live event stream · reasoning is never written · click a run header to collapse',
|
|
368
|
+
tokTitle: 'Token usage', tokNoData: 'No data yet: dispatch something and this conversation’s token usage shows up here, live.',
|
|
369
|
+
tIn: 'input', tOut: 'output', tHit: 'cache hit', tUncached: 'uncached input', tCacheRead: 'cache read', tCacheWrite: 'cache write',
|
|
370
|
+
ctxWindow: 'Context window', pctParen: (p) => ' (' + p + '%)', perRun: 'Per run',
|
|
371
|
+
lastN: (n, max) => 'last ' + n + ' · max ' + max, barTail: (i, o) => ': input ' + i + ' · output ' + o,
|
|
372
|
+
tokFoot: 'From DSH’s own session record (cumulative, including runs from before the live view).',
|
|
373
|
+
overview: 'Overview', kRuns: 'runs', kToolCalls: 'tool calls', kFailed: 'failed', kToolTime: 'tool time', kApprovals: 'approvals', kEscalations: 'escalations',
|
|
374
|
+
toolDist: 'Tools', noTools: 'No tool calls yet', failN: (n) => ' · ' + n + ' failed',
|
|
375
|
+
files: 'Files', noFiles: 'No files read or written yet', opW: 'W', opR: 'R', approvalsTitle: 'Approvals',
|
|
376
|
+
moreBelow: (n) => '↓ ' + n + ' more',
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
function pickLang() {
|
|
380
|
+
const fromUrl = (new URLSearchParams(location.search).get('lang') || '').toLowerCase();
|
|
381
|
+
if (fromUrl === 'zh' || fromUrl === 'en') return fromUrl;
|
|
382
|
+
const saved = store.get('dsh-live-lang');
|
|
383
|
+
if (saved === 'zh' || saved === 'en') return saved;
|
|
384
|
+
return /^zh/i.test(navigator.language || '') ? 'zh' : 'en';
|
|
385
|
+
}
|
|
386
|
+
let lang = pickLang();
|
|
387
|
+
let L = I18N[lang];
|
|
388
|
+
/** 切换语言:文案表、静态文字、标签映射一起换,然后重画。 */
|
|
389
|
+
function applyLang(next) {
|
|
390
|
+
lang = next;
|
|
391
|
+
L = I18N[lang];
|
|
392
|
+
({ STATE, RUN, VIA, TOOL, MODE } = L);
|
|
393
|
+
document.documentElement.lang = L.htmlLang;
|
|
394
|
+
const qi = $('q');
|
|
395
|
+
qi.placeholder = L.search;
|
|
396
|
+
qi.setAttribute('aria-label', L.searchLabel);
|
|
397
|
+
$('rail').setAttribute('aria-label', L.railLabel);
|
|
398
|
+
const btn = $('lang-btn');
|
|
399
|
+
btn.textContent = L.switchTo;
|
|
400
|
+
btn.setAttribute('aria-label', L.switchLabel);
|
|
401
|
+
btn.title = L.switchLabel;
|
|
402
|
+
$('conn-txt').textContent = connected === null ? L.connecting : connected ? L.live : L.reconnecting;
|
|
403
|
+
}
|
|
226
404
|
|
|
227
405
|
// 只用 textContent 建 DOM:事件内容来自磁盘文件,绝不当 HTML 解析。
|
|
228
406
|
function h(tag, cls, text) {
|
|
@@ -243,17 +421,21 @@ button { font: inherit; color: inherit; }
|
|
|
243
421
|
function ago(ts) {
|
|
244
422
|
if (!ts) return '';
|
|
245
423
|
const s = Math.max(0, (Date.now() - Date.parse(ts)) / 1000);
|
|
246
|
-
if (s < 45) return
|
|
247
|
-
if (s < 3600) return Math.round(s / 60)
|
|
248
|
-
if (s < 86400) return Math.round(s / 3600)
|
|
249
|
-
return Math.round(s / 86400)
|
|
424
|
+
if (s < 45) return L.justNow;
|
|
425
|
+
if (s < 3600) return L.minAgo(Math.round(s / 60));
|
|
426
|
+
if (s < 86400) return L.hrAgo(Math.round(s / 3600));
|
|
427
|
+
return L.dayAgo(Math.round(s / 86400));
|
|
250
428
|
}
|
|
251
429
|
const t = (ts) => (ts ? Date.parse(ts) : NaN);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
430
|
+
function ftok(n) {
|
|
431
|
+
if (!isFinite(n)) return '';
|
|
432
|
+
if (n < 1000) return String(Math.round(n));
|
|
433
|
+
if (n < 1e6) return (n / 1000).toFixed(n < 10000 ? 1 : 0) + 'k';
|
|
434
|
+
return (n / 1e6).toFixed(n < 1e7 ? 2 : 1) + 'M';
|
|
435
|
+
}
|
|
436
|
+
const tin = (k) => (k ? k.input + k.cache_read + k.cache_write : 0);
|
|
437
|
+
const hit = (k) => { const a = tin(k); return a > 0 ? k.cache_read / a : null; };
|
|
438
|
+
let STATE, RUN, VIA, TOOL, MODE;
|
|
257
439
|
|
|
258
440
|
function convs() {
|
|
259
441
|
const out = [];
|
|
@@ -271,11 +453,17 @@ button { font: inherit; color: inherit; }
|
|
|
271
453
|
d.append(h('b', null, String(n)), h('span', null, label));
|
|
272
454
|
box.append(d);
|
|
273
455
|
};
|
|
274
|
-
stat(s.conversations,
|
|
275
|
-
stat(s.running,
|
|
276
|
-
stat(s.queued,
|
|
277
|
-
stat(s.pending_approvals,
|
|
278
|
-
stat(s.escalations,
|
|
456
|
+
stat(s.conversations, L.sConversations, '');
|
|
457
|
+
stat(s.running, L.sRunning, 'hot');
|
|
458
|
+
stat(s.queued, L.sQueued, 'warn');
|
|
459
|
+
stat(s.pending_approvals, L.sPending, 'warn');
|
|
460
|
+
stat(s.escalations, L.sEscalations, 'warn');
|
|
461
|
+
if (s.tokens) {
|
|
462
|
+
const d = h('div', 'stat');
|
|
463
|
+
d.title = L.tokAllTitle(tin(s.tokens).toLocaleString(), s.tokens.cache_read.toLocaleString(), s.tokens.output.toLocaleString());
|
|
464
|
+
d.append(h('b', null, ftok(tin(s.tokens) + s.tokens.output)), h('span', null, L.tokStat(ftok(s.tokens.output))));
|
|
465
|
+
box.append(d);
|
|
466
|
+
}
|
|
279
467
|
}
|
|
280
468
|
|
|
281
469
|
function renderRail() {
|
|
@@ -288,9 +476,9 @@ button { font: inherit; color: inherit; }
|
|
|
288
476
|
any = true;
|
|
289
477
|
const grp = h('section', 'grp');
|
|
290
478
|
const gh = h('div', 'grp-h');
|
|
291
|
-
gh.append(h('b', null, g.client ||
|
|
479
|
+
gh.append(h('b', null, g.client || L.unknownClient));
|
|
292
480
|
gh.append(h('span', 'mono', 'pid ' + (g.pid || '?')));
|
|
293
|
-
if (!g.alive) gh.append(h('span', 'gone',
|
|
481
|
+
if (!g.alive) gh.append(h('span', 'gone', L.exited));
|
|
294
482
|
grp.append(gh);
|
|
295
483
|
for (const c of list) {
|
|
296
484
|
const row = h('div', 'cv' + (c.id === selected ? ' sel' : ''));
|
|
@@ -298,13 +486,13 @@ button { font: inherit; color: inherit; }
|
|
|
298
486
|
row.setAttribute('role', 'button');
|
|
299
487
|
row.setAttribute('aria-pressed', String(c.id === selected));
|
|
300
488
|
row.append(h('i', 'sd ' + c.state));
|
|
301
|
-
row.append(h('div', 'ti', c.title ||
|
|
489
|
+
row.append(h('div', 'ti', c.title || L.untitledShort(short(c.id))));
|
|
302
490
|
row.append(h('div', 'ago', ago(c.last_ts)));
|
|
303
491
|
const sub = h('div', 'sub');
|
|
304
492
|
const active = c.runs.find((r) => r.status === 'running');
|
|
305
493
|
const pend = c.runs.reduce((n, r) => n + r.pending_approvals, 0);
|
|
306
494
|
sub.append(document.createTextNode(active && active.current_tool ? '▸ ' + active.current_tool : STATE[c.state] || c.state));
|
|
307
|
-
if (pend) sub.append(h('span', 'flag', ' · ' + pend
|
|
495
|
+
if (pend) sub.append(h('span', 'flag', ' · ' + L.pendN(pend)));
|
|
308
496
|
sub.append(document.createTextNode(' · '));
|
|
309
497
|
const p = h('span', 'mono', base(c.cwd));
|
|
310
498
|
p.title = c.cwd || '';
|
|
@@ -319,11 +507,11 @@ button { font: inherit; color: inherit; }
|
|
|
319
507
|
}
|
|
320
508
|
if (!any) {
|
|
321
509
|
const e = h('div', 'empty');
|
|
322
|
-
if (q) e.append(h('h3', null,
|
|
510
|
+
if (q) e.append(h('h3', null, L.noMatch));
|
|
323
511
|
else {
|
|
324
|
-
e.append(h('h3', null,
|
|
512
|
+
e.append(h('h3', null, L.noEvents));
|
|
325
513
|
const p = h('p');
|
|
326
|
-
p.append(
|
|
514
|
+
p.append(L.noEventsHint[0], h('code', null, 'dsh_send'), L.noEventsHint[1]);
|
|
327
515
|
e.append(p);
|
|
328
516
|
}
|
|
329
517
|
rail.append(e);
|
|
@@ -359,7 +547,7 @@ button { font: inherit; color: inherit; }
|
|
|
359
547
|
if (it.kind === 'approval') {
|
|
360
548
|
const row = h('div', 'span flagged');
|
|
361
549
|
row.append(h('span', 'st', '◆'));
|
|
362
|
-
row.append(h('span', 'nm',
|
|
550
|
+
row.append(h('span', 'nm', L.approval));
|
|
363
551
|
const inp = h('span', 'in');
|
|
364
552
|
if (it.escalation) inp.append(h('span', 'esc', '⚠ ' + it.escalation));
|
|
365
553
|
const txt = it.command_preview || it.title || it.tool || '';
|
|
@@ -368,12 +556,12 @@ button { font: inherit; color: inherit; }
|
|
|
368
556
|
row.append(inp);
|
|
369
557
|
const d = it.decided;
|
|
370
558
|
row.append(h('span', 'in', d ? '→ ' + (d.decision || d.effective || '') + (d.by ? '(' + d.by + ')' : '') : MODE[it.mode] || it.mode || ''));
|
|
371
|
-
row.append(h('span', 'tm', it.mode === 'ask' && !d ?
|
|
559
|
+
row.append(h('span', 'tm', it.mode === 'ask' && !d ? L.waiting : ''));
|
|
372
560
|
return row;
|
|
373
561
|
}
|
|
374
562
|
const row = h('div', 'span note' + (it.kind === 'interrupt' ? ' stop' : ''));
|
|
375
563
|
row.append(h('span', 'st', it.kind === 'interrupt' ? '■' : '↪'));
|
|
376
|
-
row.append(h('span', 'nm', it.kind === 'interrupt' ?
|
|
564
|
+
row.append(h('span', 'nm', it.kind === 'interrupt' ? L.interrupted : L.interject + (it.mode ? ' · ' + it.mode : '')));
|
|
377
565
|
row.append(h('span', 'in', it.text_preview || ''));
|
|
378
566
|
row.append(h('span'));
|
|
379
567
|
row.append(h('span', 'tm', ''));
|
|
@@ -391,7 +579,7 @@ button { font: inherit; color: inherit; }
|
|
|
391
579
|
const b = h('b');
|
|
392
580
|
b.style.left = left + '%';
|
|
393
581
|
b.style.width = width + '%';
|
|
394
|
-
tr.title = dur(start - t0)
|
|
582
|
+
tr.title = L.trackTitle(dur(start - t0), dur(end - start));
|
|
395
583
|
tr.append(b);
|
|
396
584
|
return tr;
|
|
397
585
|
}
|
|
@@ -413,9 +601,9 @@ button { font: inherit; color: inherit; }
|
|
|
413
601
|
if (!c) c = all.find((x) => x.state === 'running') || all[0];
|
|
414
602
|
if (!c) {
|
|
415
603
|
const e = h('div', 'empty');
|
|
416
|
-
e.append(h('h3', null,
|
|
604
|
+
e.append(h('h3', null, L.waitingFirst));
|
|
417
605
|
const p = h('p');
|
|
418
|
-
p.append(
|
|
606
|
+
p.append(L.firstHint[0], h('code', null, 'live/'), L.firstHint[1]);
|
|
419
607
|
e.append(p);
|
|
420
608
|
main.append(e);
|
|
421
609
|
return;
|
|
@@ -423,7 +611,7 @@ button { font: inherit; color: inherit; }
|
|
|
423
611
|
selected = c.id;
|
|
424
612
|
const wrap = h('div', 'wrap');
|
|
425
613
|
const hd = h('div', 'hd');
|
|
426
|
-
hd.append(h('h1', null, c.title ||
|
|
614
|
+
hd.append(h('h1', null, c.title || L.untitledConv));
|
|
427
615
|
const facts = h('dl', 'facts');
|
|
428
616
|
const fact = (k, v, cls, full) => {
|
|
429
617
|
if (!v) return;
|
|
@@ -438,24 +626,26 @@ button { font: inherit; color: inherit; }
|
|
|
438
626
|
f.append(dd);
|
|
439
627
|
facts.append(f);
|
|
440
628
|
};
|
|
441
|
-
fact(
|
|
442
|
-
fact(
|
|
443
|
-
fact(
|
|
444
|
-
fact(
|
|
445
|
-
fact(
|
|
446
|
-
fact(
|
|
629
|
+
fact(L.fState, STATE[c.state] || c.state);
|
|
630
|
+
fact(L.fClient, (c.client || L.unknown) + ' · pid ' + (c.writer ? String(c.writer).split(':')[0] : '?') + (c.writer_alive ? '' : L.exitedParen));
|
|
631
|
+
fact(L.fPermission, [c.permission, c.on_approval].filter(Boolean).join(' · '));
|
|
632
|
+
fact(L.fModel, c.model);
|
|
633
|
+
fact(L.fConvId, short(c.id), 'mono', c.id);
|
|
634
|
+
fact(L.fDir, c.cwd, 'mono path');
|
|
447
635
|
hd.append(facts);
|
|
448
636
|
wrap.append(hd);
|
|
449
637
|
|
|
638
|
+
const cols = h('div', 'cols');
|
|
639
|
+
const left = h('div');
|
|
450
640
|
const sh = h('div', 'sec-h');
|
|
451
|
-
sh.append(h('h2', null,
|
|
452
|
-
|
|
641
|
+
sh.append(h('h2', null, L.chain), h('span', null, L.runsCount(c.runs.length)));
|
|
642
|
+
left.append(sh);
|
|
453
643
|
|
|
454
644
|
const turns = h('div', 'turns');
|
|
455
645
|
const runs = [...c.runs].reverse();
|
|
456
646
|
if (!runs.length) {
|
|
457
647
|
const e = h('div', 'empty');
|
|
458
|
-
e.append(h('h3', null,
|
|
648
|
+
e.append(h('h3', null, L.noRuns));
|
|
459
649
|
turns.append(e);
|
|
460
650
|
}
|
|
461
651
|
for (const r of runs) {
|
|
@@ -469,15 +659,21 @@ button { font: inherit; color: inherit; }
|
|
|
469
659
|
head.append(chev());
|
|
470
660
|
head.append(h('span', 'tag ' + r.status, RUN[r.status] || r.status));
|
|
471
661
|
if (r.via && !(r.status === 'queued' && r.via === 'queue')) head.append(h('span', 'tag ghost', VIA[r.via] || r.via));
|
|
472
|
-
head.append(h('span', 'rid', (r.turn ?
|
|
473
|
-
if (r.escalations) head.append(h('span', 'tag warn',
|
|
474
|
-
if (r.pending_approvals) head.append(h('span', 'tag warn', r.pending_approvals
|
|
662
|
+
head.append(h('span', 'rid', (r.turn ? L.turnN(r.turn) : '') + r.run));
|
|
663
|
+
if (r.escalations) head.append(h('span', 'tag warn', L.escN(r.escalations)));
|
|
664
|
+
if (r.pending_approvals) head.append(h('span', 'tag warn', L.pendN(r.pending_approvals)));
|
|
475
665
|
const right = h('span', 'right');
|
|
476
666
|
const tools = r.items.filter((x) => x.kind === 'tool').length;
|
|
477
667
|
const n = r.tool_count != null ? r.tool_count : tools;
|
|
478
|
-
const rb = h('span'); rb.append(h('b', null, String(n)),
|
|
668
|
+
const rb = h('span'); rb.append(h('b', null, String(n)), L.toolsSuffix(n)); right.append(rb);
|
|
479
669
|
const t0 = t(r.started_at);
|
|
480
670
|
const total = r.duration_ms != null ? r.duration_ms : r.status === 'running' && isFinite(t0) ? Date.now() - t0 : null;
|
|
671
|
+
if (r.tokens) {
|
|
672
|
+
const tk = h('span', 'tok-run');
|
|
673
|
+
tk.title = L.tokRunTitle(tin(r.tokens), r.tokens.cache_read, r.tokens.output);
|
|
674
|
+
tk.append(h('b', null, ftok(tin(r.tokens))), '↑ ', h('b', null, ftok(r.tokens.output)), '↓');
|
|
675
|
+
right.append(tk);
|
|
676
|
+
}
|
|
481
677
|
if (total != null) { const d = h('span'); d.append(h('b', null, dur(total))); right.append(d); }
|
|
482
678
|
right.append(h('span', null, ago(r.ended_at || r.started_at || r.queued_at)));
|
|
483
679
|
head.append(right);
|
|
@@ -489,19 +685,238 @@ button { font: inherit; color: inherit; }
|
|
|
489
685
|
if (r.prompt_preview) body.append(h('div', 'prompt', r.prompt_preview));
|
|
490
686
|
if (r.error) body.append(h('div', 'errline', r.error));
|
|
491
687
|
if (r.items.length) {
|
|
688
|
+
const boxEl = h('div', 'spans-box');
|
|
492
689
|
const spans = h('div', 'spans');
|
|
690
|
+
spans.dataset.key = key;
|
|
691
|
+
spans.tabIndex = 0; // 键盘也能滚(方向键 / PageDown)
|
|
692
|
+
spans.setAttribute('aria-label', L.toolList);
|
|
493
693
|
for (const it of r.items) spans.append(spanRow(it, r, t0, total, c.cwd));
|
|
494
|
-
|
|
694
|
+
boxEl.append(spans, h('span', 'spans-count'));
|
|
695
|
+
body.append(boxEl);
|
|
696
|
+
pendingScroll.push({ spans, box: boxEl, key, running: r.status === 'running' });
|
|
495
697
|
}
|
|
496
698
|
card.append(body);
|
|
497
699
|
turn.append(card);
|
|
498
700
|
turns.append(turn);
|
|
499
701
|
}
|
|
500
|
-
|
|
501
|
-
|
|
702
|
+
left.append(turns);
|
|
703
|
+
left.append(h('div', 'foot', L.foot));
|
|
704
|
+
cols.append(left, inspector(c));
|
|
705
|
+
wrap.append(cols);
|
|
502
706
|
main.append(wrap);
|
|
503
707
|
}
|
|
504
708
|
|
|
709
|
+
/** Token 面板:会话累计(DSH 记录)、构成、上下文窗口、每回合用量。 */
|
|
710
|
+
function tokenPanel(c) {
|
|
711
|
+
const p = h('section', 'panel');
|
|
712
|
+
p.append(h('h3', null, L.tokTitle));
|
|
713
|
+
const k = c.tokens;
|
|
714
|
+
if (!k) {
|
|
715
|
+
p.append(h('div', 'muted', L.tokNoData));
|
|
716
|
+
return p;
|
|
717
|
+
}
|
|
718
|
+
const big = h('div', 'tok-big');
|
|
719
|
+
const cell = (v, label) => { const d = h('div'); d.append(h('b', null, v), h('span', null, label)); big.append(d); };
|
|
720
|
+
cell(ftok(tin(k)), L.tIn);
|
|
721
|
+
cell(ftok(k.output), L.tOut);
|
|
722
|
+
const hr = hit(k);
|
|
723
|
+
cell(hr == null ? '—' : Math.round(hr * 100) + '%', L.tHit);
|
|
724
|
+
p.append(big);
|
|
725
|
+
|
|
726
|
+
// 构成:未命中缓存的输入 / 缓存读 / 缓存写 / 输出
|
|
727
|
+
const parts = [
|
|
728
|
+
['c-unc', L.tUncached, k.input],
|
|
729
|
+
['c-read', L.tCacheRead, k.cache_read],
|
|
730
|
+
['c-write', L.tCacheWrite, k.cache_write],
|
|
731
|
+
['c-out', L.tOut, k.output],
|
|
732
|
+
];
|
|
733
|
+
const sum = parts.reduce((n, x) => n + x[2], 0) || 1;
|
|
734
|
+
const stack = h('div', 'stack');
|
|
735
|
+
stack.setAttribute('role', 'img');
|
|
736
|
+
stack.setAttribute('aria-label', parts.map((x) => x[1] + ' ' + x[2]).join(','));
|
|
737
|
+
for (const [cls, label, v] of parts) {
|
|
738
|
+
if (!v) continue;
|
|
739
|
+
const i = h('i', cls);
|
|
740
|
+
i.style.width = (v / sum) * 100 + '%';
|
|
741
|
+
i.title = label + ' ' + v.toLocaleString();
|
|
742
|
+
stack.append(i);
|
|
743
|
+
}
|
|
744
|
+
p.append(stack);
|
|
745
|
+
const lg = h('div', 'legend');
|
|
746
|
+
for (const [cls, label, v] of parts) {
|
|
747
|
+
const s = h('span');
|
|
748
|
+
s.append(h('i', cls), label, h('em', null, ftok(v)));
|
|
749
|
+
lg.append(s);
|
|
750
|
+
}
|
|
751
|
+
p.append(lg);
|
|
752
|
+
|
|
753
|
+
if (c.context && c.context.window) {
|
|
754
|
+
const pct = Math.min(100, (c.context.used / c.context.window) * 100);
|
|
755
|
+
const hh = h('div', 'ctx-h');
|
|
756
|
+
hh.append(h('span', null, L.ctxWindow), h('span', null, ftok(c.context.used) + ' / ' + ftok(c.context.window) + L.pctParen(pct < 1 ? pct.toFixed(1) : Math.round(pct))));
|
|
757
|
+
p.append(hh);
|
|
758
|
+
const bar = h('div', 'ctx' + (pct > 85 ? ' crit' : pct > 60 ? ' hi' : ''));
|
|
759
|
+
const b = h('b'); b.style.width = Math.max(1, pct) + '%'; bar.append(b);
|
|
760
|
+
p.append(bar);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const runs = c.runs.filter((r) => r.tokens).slice(-16);
|
|
764
|
+
if (runs.length) {
|
|
765
|
+
const per = runs.map((r) => tin(r.tokens) + r.tokens.output);
|
|
766
|
+
const max = Math.max(...per) || 1;
|
|
767
|
+
const sh = h('div', 'spark-h');
|
|
768
|
+
sh.append(h('span', null, L.perRun), h('span', null, L.lastN(runs.length, ftok(max))));
|
|
769
|
+
p.append(sh);
|
|
770
|
+
const sp = h('div', 'spark');
|
|
771
|
+
runs.forEach((r, i) => {
|
|
772
|
+
const bar = h('i', r.status === 'running' ? 'cur' : '');
|
|
773
|
+
bar.style.height = Math.max(6, (per[i] / max) * 100) + '%';
|
|
774
|
+
bar.title = (r.turn ? L.turnN(r.turn) : '') + r.run + L.barTail(ftok(tin(r.tokens)), ftok(r.tokens.output));
|
|
775
|
+
sp.append(bar);
|
|
776
|
+
});
|
|
777
|
+
p.append(sp);
|
|
778
|
+
}
|
|
779
|
+
p.append(h('div', 'foot', L.tokFoot));
|
|
780
|
+
return p;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** 读写类工具:用来从输入预览里认出"涉及的文件"。 */
|
|
784
|
+
// 只认真正读文件的工具;grep / glob / search / ls 的输入是目录或模式,不算"涉及的文件"。
|
|
785
|
+
const READ_TOOLS = /^(read|view|cat|open)/i;
|
|
786
|
+
const WRITE_TOOLS = /^(write|edit|create|patch|apply|replace|delete|move|rename|mkdir)/i;
|
|
787
|
+
const looksLikePath = (s) => !!s && !/\s/.test(s) && (/[\\/]/.test(s) || /\.[A-Za-z0-9]{1,8}$/.test(s));
|
|
788
|
+
|
|
789
|
+
/** 右侧检查器:这个会话的总量、工具分布、涉及文件、审批。 */
|
|
790
|
+
function inspector(c) {
|
|
791
|
+
const box = h('aside', 'inspector');
|
|
792
|
+
const tools = [];
|
|
793
|
+
const approvals = [];
|
|
794
|
+
for (const r of c.runs) for (const it of r.items) {
|
|
795
|
+
if (it.kind === 'tool') tools.push(it);
|
|
796
|
+
else if (it.kind === 'approval') approvals.push(it);
|
|
797
|
+
}
|
|
798
|
+
const failed = tools.filter((x) => x.status === 'failed').length;
|
|
799
|
+
const toolMs = tools.reduce((n, x) => n + (x.elapsed_ms || 0), 0);
|
|
800
|
+
const esc = approvals.filter((x) => x.escalation).length;
|
|
801
|
+
|
|
802
|
+
const ov = h('section', 'panel');
|
|
803
|
+
ov.append(h('h3', null, L.overview));
|
|
804
|
+
const kpis = h('div', 'kpis');
|
|
805
|
+
const kpi = (v, label, cls) => {
|
|
806
|
+
const k = h('div', 'kpi ' + (v && v !== '0' ? cls || '' : 'zero'));
|
|
807
|
+
k.append(h('b', null, String(v)), h('span', null, label));
|
|
808
|
+
kpis.append(k);
|
|
809
|
+
};
|
|
810
|
+
kpi(c.runs.length, L.kRuns);
|
|
811
|
+
kpi(tools.length, L.kToolCalls);
|
|
812
|
+
kpi(failed, L.kFailed, 'bad');
|
|
813
|
+
kpi(toolMs ? dur(toolMs) : '0', L.kToolTime);
|
|
814
|
+
kpi(approvals.length, L.kApprovals, 'warn');
|
|
815
|
+
kpi(esc, L.kEscalations, 'warn');
|
|
816
|
+
ov.append(kpis);
|
|
817
|
+
box.append(ov);
|
|
818
|
+
box.append(tokenPanel(c));
|
|
819
|
+
|
|
820
|
+
const byName = new Map();
|
|
821
|
+
for (const x of tools) {
|
|
822
|
+
const k = x.name || x.title || 'tool';
|
|
823
|
+
const e = byName.get(k) || { n: 0, ms: 0, fail: 0 };
|
|
824
|
+
e.n++; e.ms += x.elapsed_ms || 0; if (x.status === 'failed') e.fail++;
|
|
825
|
+
byName.set(k, e);
|
|
826
|
+
}
|
|
827
|
+
const dist = h('section', 'panel');
|
|
828
|
+
dist.append(h('h3', null, L.toolDist));
|
|
829
|
+
if (!byName.size) dist.append(h('div', 'muted', L.noTools));
|
|
830
|
+
else {
|
|
831
|
+
const rows = [...byName.entries()].sort((a, b) => b[1].n - a[1].n || b[1].ms - a[1].ms);
|
|
832
|
+
const max = rows[0][1].n;
|
|
833
|
+
const list = h('div', 'dist');
|
|
834
|
+
for (const [name, e] of rows.slice(0, 8)) {
|
|
835
|
+
const row = h('div', 'dist-row');
|
|
836
|
+
row.append(h('span', 'n', name));
|
|
837
|
+
row.append(h('span', 'v', '×' + e.n + (e.ms ? ' · ' + dur(e.ms) : '') + (e.fail ? L.failN(e.fail) : '')));
|
|
838
|
+
const bar = h('span', 'bar'); const b = h('b'); b.style.width = Math.max(4, (e.n / max) * 100) + '%'; bar.append(b); row.append(bar);
|
|
839
|
+
list.append(row);
|
|
840
|
+
}
|
|
841
|
+
dist.append(list);
|
|
842
|
+
}
|
|
843
|
+
box.append(dist);
|
|
844
|
+
|
|
845
|
+
const files = new Map();
|
|
846
|
+
for (const x of tools) {
|
|
847
|
+
const name = x.name || x.title || '';
|
|
848
|
+
const p = x.input_preview && x.input_preview.split(' · ')[0];
|
|
849
|
+
if (!looksLikePath(p)) continue;
|
|
850
|
+
const op = WRITE_TOOLS.test(name) ? 'w' : READ_TOOLS.test(name) ? 'r' : null;
|
|
851
|
+
if (!op) continue;
|
|
852
|
+
const key = rel(p, c.cwd) || p;
|
|
853
|
+
const prev = files.get(key);
|
|
854
|
+
files.set(key, prev === 'w' || op === 'w' ? 'w' : 'r');
|
|
855
|
+
}
|
|
856
|
+
const fp = h('section', 'panel');
|
|
857
|
+
fp.append(h('h3', null, L.files));
|
|
858
|
+
if (!files.size) fp.append(h('div', 'muted', L.noFiles));
|
|
859
|
+
else {
|
|
860
|
+
const ul = h('ul', 'files');
|
|
861
|
+
for (const [p, op] of [...files.entries()].slice(-14).reverse()) {
|
|
862
|
+
const li = h('li');
|
|
863
|
+
li.append(h('span', 'op ' + op, op === 'w' ? L.opW : L.opR));
|
|
864
|
+
const ps = h('span', 'p'); const bdi = document.createElement('bdi'); bdi.textContent = p; ps.append(bdi); ps.title = p;
|
|
865
|
+
li.append(ps);
|
|
866
|
+
ul.append(li);
|
|
867
|
+
}
|
|
868
|
+
fp.append(ul);
|
|
869
|
+
}
|
|
870
|
+
box.append(fp);
|
|
871
|
+
|
|
872
|
+
if (approvals.length) {
|
|
873
|
+
const ap = h('section', 'panel');
|
|
874
|
+
ap.append(h('h3', null, L.approvalsTitle));
|
|
875
|
+
const list = h('div', 'aps');
|
|
876
|
+
for (const a of approvals.slice(-6).reverse()) {
|
|
877
|
+
const d = h('div', 'ap');
|
|
878
|
+
const m = h('div', 'm', (MODE[a.mode] || a.mode || '') + (a.escalation ? ' · ⚠ ' + a.escalation : ''));
|
|
879
|
+
d.append(m);
|
|
880
|
+
const cmd = rel(a.command_preview || a.title || a.tool || '', c.cwd);
|
|
881
|
+
if (cmd) { const cc = h('div', 'c', cmd); cc.title = cmd; d.append(cc); }
|
|
882
|
+
if (a.decided) d.append(h('div', 'muted', '→ ' + (a.decided.decision || a.decided.effective || '') + (a.decided.by ? '(' + a.decided.by + ')' : '')));
|
|
883
|
+
list.append(d);
|
|
884
|
+
}
|
|
885
|
+
ap.append(list);
|
|
886
|
+
box.append(ap);
|
|
887
|
+
}
|
|
888
|
+
return box;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* 每个回合工具列表的滚动状态(按 会话/回合 记):整块重建 DOM 后恢复位置。
|
|
893
|
+
* 停在底部的(或从没滚过的运行中回合)跟随最新工具;用户往上翻过就停在原处。
|
|
894
|
+
*/
|
|
895
|
+
const scrollState = new Map();
|
|
896
|
+
let pendingScroll = [];
|
|
897
|
+
function updateEdges(spans, box) {
|
|
898
|
+
const max = spans.scrollHeight - spans.clientHeight;
|
|
899
|
+
box.classList.toggle('more-up', spans.scrollTop > 2);
|
|
900
|
+
box.classList.toggle('more-down', spans.scrollTop < max - 2);
|
|
901
|
+
const hiddenBelow = Math.max(0, Math.round((max - spans.scrollTop) / 32));
|
|
902
|
+
box.lastElementChild.textContent = hiddenBelow ? L.moreBelow(hiddenBelow) : '';
|
|
903
|
+
}
|
|
904
|
+
function restoreScroll() {
|
|
905
|
+
for (const { spans, box, key, running } of pendingScroll) {
|
|
906
|
+
const st = scrollState.get(key);
|
|
907
|
+
const max = spans.scrollHeight - spans.clientHeight;
|
|
908
|
+
if (st && !st.atBottom) spans.scrollTop = Math.min(st.top, max);
|
|
909
|
+
else if (st || running) spans.scrollTop = max;
|
|
910
|
+
updateEdges(spans, box);
|
|
911
|
+
spans.addEventListener('scroll', () => {
|
|
912
|
+
const m = spans.scrollHeight - spans.clientHeight;
|
|
913
|
+
scrollState.set(key, { top: spans.scrollTop, atBottom: spans.scrollTop >= m - 4 });
|
|
914
|
+
updateEdges(spans, box);
|
|
915
|
+
}, { passive: true });
|
|
916
|
+
}
|
|
917
|
+
pendingScroll = [];
|
|
918
|
+
}
|
|
919
|
+
|
|
505
920
|
function render() {
|
|
506
921
|
if (!view) return;
|
|
507
922
|
const rt = $('rail').scrollTop, mt = $('main').scrollTop, wt = window.scrollY;
|
|
@@ -513,11 +928,20 @@ button { font: inherit; color: inherit; }
|
|
|
513
928
|
}
|
|
514
929
|
renderStats();
|
|
515
930
|
renderRail();
|
|
931
|
+
pendingScroll = [];
|
|
516
932
|
renderMain();
|
|
933
|
+
restoreScroll();
|
|
517
934
|
$('rail').scrollTop = rt; $('main').scrollTop = mt; window.scrollTo(0, wt);
|
|
518
935
|
}
|
|
519
936
|
|
|
520
937
|
$('q').addEventListener('input', (e) => { q = e.target.value.trim().toLowerCase(); render(); });
|
|
938
|
+
$('lang-btn').addEventListener('click', () => {
|
|
939
|
+
const next = lang === 'zh' ? 'en' : 'zh';
|
|
940
|
+
store.set('dsh-live-lang', next);
|
|
941
|
+
applyLang(next);
|
|
942
|
+
render();
|
|
943
|
+
});
|
|
944
|
+
applyLang(lang);
|
|
521
945
|
|
|
522
946
|
function setConn(on, text) {
|
|
523
947
|
const c = $('conn');
|
|
@@ -525,9 +949,9 @@ button { font: inherit; color: inherit; }
|
|
|
525
949
|
c.lastElementChild.textContent = text;
|
|
526
950
|
}
|
|
527
951
|
const es = new EventSource('/events?t=' + encodeURIComponent(token));
|
|
528
|
-
es.onopen = () => setConn(true,
|
|
952
|
+
es.onopen = () => { connected = true; setConn(true, L.live); };
|
|
529
953
|
es.onmessage = (e) => { try { view = JSON.parse(e.data); } catch { return; } render(); };
|
|
530
|
-
es.onerror = () => setConn(false,
|
|
954
|
+
es.onerror = () => { connected = false; setConn(false, L.reconnecting); };
|
|
531
955
|
setInterval(render, 15000);
|
|
532
956
|
})();
|
|
533
957
|
</script>
|