dsh-any-agent 0.6.2 → 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 +60 -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 +2 -1
- package/docs/zh/architecture.md +12 -0
- package/docs/zh/limits.md +6 -0
- package/docs/zh/live-view-design.md +11 -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 +2 -1
- package/package.json +6 -5
- package/src/doctor.mjs +63 -2
- package/src/live-cli.mjs +52 -15
- package/src/live-dashboard.html +191 -79
- package/src/live-view.mjs +111 -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
|
@@ -64,6 +64,9 @@ button { font: inherit; color: inherit; }
|
|
|
64
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; }
|
|
65
65
|
.search input:focus { border-color: var(--accent); background: var(--surface); }
|
|
66
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; }
|
|
67
70
|
|
|
68
71
|
/* ── 布局 ───────────────────────────────── */
|
|
69
72
|
.layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); height: calc(100% - 52px); }
|
|
@@ -268,16 +271,17 @@ button { font: inherit; color: inherit; }
|
|
|
268
271
|
</head>
|
|
269
272
|
<body>
|
|
270
273
|
<header class="top">
|
|
271
|
-
<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>
|
|
272
275
|
<div class="stats" id="stats"></div>
|
|
273
276
|
<div class="grow"></div>
|
|
274
277
|
<label class="search">
|
|
275
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>
|
|
276
|
-
<input type="search" id="q"
|
|
279
|
+
<input type="search" id="q">
|
|
277
280
|
</label>
|
|
281
|
+
<button type="button" class="lang-btn" id="lang-btn"></button>
|
|
278
282
|
</header>
|
|
279
283
|
<div class="layout">
|
|
280
|
-
<nav class="rail" id="rail"
|
|
284
|
+
<nav class="rail" id="rail"></nav>
|
|
281
285
|
<main class="main" id="main" aria-live="polite"></main>
|
|
282
286
|
</div>
|
|
283
287
|
<script>
|
|
@@ -293,6 +297,110 @@ button { font: inherit; color: inherit; }
|
|
|
293
297
|
let selected = store.get('dsh-live-sel');
|
|
294
298
|
const collapsed = new Set();
|
|
295
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
|
+
}
|
|
296
404
|
|
|
297
405
|
// 只用 textContent 建 DOM:事件内容来自磁盘文件,绝不当 HTML 解析。
|
|
298
406
|
function h(tag, cls, text) {
|
|
@@ -313,10 +421,10 @@ button { font: inherit; color: inherit; }
|
|
|
313
421
|
function ago(ts) {
|
|
314
422
|
if (!ts) return '';
|
|
315
423
|
const s = Math.max(0, (Date.now() - Date.parse(ts)) / 1000);
|
|
316
|
-
if (s < 45) return
|
|
317
|
-
if (s < 3600) return Math.round(s / 60)
|
|
318
|
-
if (s < 86400) return Math.round(s / 3600)
|
|
319
|
-
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));
|
|
320
428
|
}
|
|
321
429
|
const t = (ts) => (ts ? Date.parse(ts) : NaN);
|
|
322
430
|
function ftok(n) {
|
|
@@ -327,11 +435,7 @@ button { font: inherit; color: inherit; }
|
|
|
327
435
|
}
|
|
328
436
|
const tin = (k) => (k ? k.input + k.cache_read + k.cache_write : 0);
|
|
329
437
|
const hit = (k) => { const a = tin(k); return a > 0 ? k.cache_read / a : null; };
|
|
330
|
-
|
|
331
|
-
const RUN = { running: '运行中', queued: '排队', done: '完成', error: '出错', cancelled: '已取消', interrupted: '中断' };
|
|
332
|
-
const VIA = { send: '派活', queue: '排队', interject: '插话' };
|
|
333
|
-
const TOOL = { pending: '等待', in_progress: '进行中', unfinished: '未完成', completed: '完成', failed: '失败' };
|
|
334
|
-
const MODE = { 'auto-allow': '自动放行', 'auto-deny': '自动拒绝', 'read-only-deny': '只读拒绝', ask: '待人工裁决' };
|
|
438
|
+
let STATE, RUN, VIA, TOOL, MODE;
|
|
335
439
|
|
|
336
440
|
function convs() {
|
|
337
441
|
const out = [];
|
|
@@ -349,15 +453,15 @@ button { font: inherit; color: inherit; }
|
|
|
349
453
|
d.append(h('b', null, String(n)), h('span', null, label));
|
|
350
454
|
box.append(d);
|
|
351
455
|
};
|
|
352
|
-
stat(s.conversations,
|
|
353
|
-
stat(s.running,
|
|
354
|
-
stat(s.queued,
|
|
355
|
-
stat(s.pending_approvals,
|
|
356
|
-
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');
|
|
357
461
|
if (s.tokens) {
|
|
358
462
|
const d = h('div', 'stat');
|
|
359
|
-
d.title =
|
|
360
|
-
d.append(h('b', null, ftok(tin(s.tokens) + s.tokens.output)), h('span', null,
|
|
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))));
|
|
361
465
|
box.append(d);
|
|
362
466
|
}
|
|
363
467
|
}
|
|
@@ -372,9 +476,9 @@ button { font: inherit; color: inherit; }
|
|
|
372
476
|
any = true;
|
|
373
477
|
const grp = h('section', 'grp');
|
|
374
478
|
const gh = h('div', 'grp-h');
|
|
375
|
-
gh.append(h('b', null, g.client ||
|
|
479
|
+
gh.append(h('b', null, g.client || L.unknownClient));
|
|
376
480
|
gh.append(h('span', 'mono', 'pid ' + (g.pid || '?')));
|
|
377
|
-
if (!g.alive) gh.append(h('span', 'gone',
|
|
481
|
+
if (!g.alive) gh.append(h('span', 'gone', L.exited));
|
|
378
482
|
grp.append(gh);
|
|
379
483
|
for (const c of list) {
|
|
380
484
|
const row = h('div', 'cv' + (c.id === selected ? ' sel' : ''));
|
|
@@ -382,13 +486,13 @@ button { font: inherit; color: inherit; }
|
|
|
382
486
|
row.setAttribute('role', 'button');
|
|
383
487
|
row.setAttribute('aria-pressed', String(c.id === selected));
|
|
384
488
|
row.append(h('i', 'sd ' + c.state));
|
|
385
|
-
row.append(h('div', 'ti', c.title ||
|
|
489
|
+
row.append(h('div', 'ti', c.title || L.untitledShort(short(c.id))));
|
|
386
490
|
row.append(h('div', 'ago', ago(c.last_ts)));
|
|
387
491
|
const sub = h('div', 'sub');
|
|
388
492
|
const active = c.runs.find((r) => r.status === 'running');
|
|
389
493
|
const pend = c.runs.reduce((n, r) => n + r.pending_approvals, 0);
|
|
390
494
|
sub.append(document.createTextNode(active && active.current_tool ? '▸ ' + active.current_tool : STATE[c.state] || c.state));
|
|
391
|
-
if (pend) sub.append(h('span', 'flag', ' · ' + pend
|
|
495
|
+
if (pend) sub.append(h('span', 'flag', ' · ' + L.pendN(pend)));
|
|
392
496
|
sub.append(document.createTextNode(' · '));
|
|
393
497
|
const p = h('span', 'mono', base(c.cwd));
|
|
394
498
|
p.title = c.cwd || '';
|
|
@@ -403,11 +507,11 @@ button { font: inherit; color: inherit; }
|
|
|
403
507
|
}
|
|
404
508
|
if (!any) {
|
|
405
509
|
const e = h('div', 'empty');
|
|
406
|
-
if (q) e.append(h('h3', null,
|
|
510
|
+
if (q) e.append(h('h3', null, L.noMatch));
|
|
407
511
|
else {
|
|
408
|
-
e.append(h('h3', null,
|
|
512
|
+
e.append(h('h3', null, L.noEvents));
|
|
409
513
|
const p = h('p');
|
|
410
|
-
p.append(
|
|
514
|
+
p.append(L.noEventsHint[0], h('code', null, 'dsh_send'), L.noEventsHint[1]);
|
|
411
515
|
e.append(p);
|
|
412
516
|
}
|
|
413
517
|
rail.append(e);
|
|
@@ -443,7 +547,7 @@ button { font: inherit; color: inherit; }
|
|
|
443
547
|
if (it.kind === 'approval') {
|
|
444
548
|
const row = h('div', 'span flagged');
|
|
445
549
|
row.append(h('span', 'st', '◆'));
|
|
446
|
-
row.append(h('span', 'nm',
|
|
550
|
+
row.append(h('span', 'nm', L.approval));
|
|
447
551
|
const inp = h('span', 'in');
|
|
448
552
|
if (it.escalation) inp.append(h('span', 'esc', '⚠ ' + it.escalation));
|
|
449
553
|
const txt = it.command_preview || it.title || it.tool || '';
|
|
@@ -452,12 +556,12 @@ button { font: inherit; color: inherit; }
|
|
|
452
556
|
row.append(inp);
|
|
453
557
|
const d = it.decided;
|
|
454
558
|
row.append(h('span', 'in', d ? '→ ' + (d.decision || d.effective || '') + (d.by ? '(' + d.by + ')' : '') : MODE[it.mode] || it.mode || ''));
|
|
455
|
-
row.append(h('span', 'tm', it.mode === 'ask' && !d ?
|
|
559
|
+
row.append(h('span', 'tm', it.mode === 'ask' && !d ? L.waiting : ''));
|
|
456
560
|
return row;
|
|
457
561
|
}
|
|
458
562
|
const row = h('div', 'span note' + (it.kind === 'interrupt' ? ' stop' : ''));
|
|
459
563
|
row.append(h('span', 'st', it.kind === 'interrupt' ? '■' : '↪'));
|
|
460
|
-
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 : '')));
|
|
461
565
|
row.append(h('span', 'in', it.text_preview || ''));
|
|
462
566
|
row.append(h('span'));
|
|
463
567
|
row.append(h('span', 'tm', ''));
|
|
@@ -475,7 +579,7 @@ button { font: inherit; color: inherit; }
|
|
|
475
579
|
const b = h('b');
|
|
476
580
|
b.style.left = left + '%';
|
|
477
581
|
b.style.width = width + '%';
|
|
478
|
-
tr.title = dur(start - t0)
|
|
582
|
+
tr.title = L.trackTitle(dur(start - t0), dur(end - start));
|
|
479
583
|
tr.append(b);
|
|
480
584
|
return tr;
|
|
481
585
|
}
|
|
@@ -497,9 +601,9 @@ button { font: inherit; color: inherit; }
|
|
|
497
601
|
if (!c) c = all.find((x) => x.state === 'running') || all[0];
|
|
498
602
|
if (!c) {
|
|
499
603
|
const e = h('div', 'empty');
|
|
500
|
-
e.append(h('h3', null,
|
|
604
|
+
e.append(h('h3', null, L.waitingFirst));
|
|
501
605
|
const p = h('p');
|
|
502
|
-
p.append(
|
|
606
|
+
p.append(L.firstHint[0], h('code', null, 'live/'), L.firstHint[1]);
|
|
503
607
|
e.append(p);
|
|
504
608
|
main.append(e);
|
|
505
609
|
return;
|
|
@@ -507,7 +611,7 @@ button { font: inherit; color: inherit; }
|
|
|
507
611
|
selected = c.id;
|
|
508
612
|
const wrap = h('div', 'wrap');
|
|
509
613
|
const hd = h('div', 'hd');
|
|
510
|
-
hd.append(h('h1', null, c.title ||
|
|
614
|
+
hd.append(h('h1', null, c.title || L.untitledConv));
|
|
511
615
|
const facts = h('dl', 'facts');
|
|
512
616
|
const fact = (k, v, cls, full) => {
|
|
513
617
|
if (!v) return;
|
|
@@ -522,26 +626,26 @@ button { font: inherit; color: inherit; }
|
|
|
522
626
|
f.append(dd);
|
|
523
627
|
facts.append(f);
|
|
524
628
|
};
|
|
525
|
-
fact(
|
|
526
|
-
fact(
|
|
527
|
-
fact(
|
|
528
|
-
fact(
|
|
529
|
-
fact(
|
|
530
|
-
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');
|
|
531
635
|
hd.append(facts);
|
|
532
636
|
wrap.append(hd);
|
|
533
637
|
|
|
534
638
|
const cols = h('div', 'cols');
|
|
535
639
|
const left = h('div');
|
|
536
640
|
const sh = h('div', 'sec-h');
|
|
537
|
-
sh.append(h('h2', null,
|
|
641
|
+
sh.append(h('h2', null, L.chain), h('span', null, L.runsCount(c.runs.length)));
|
|
538
642
|
left.append(sh);
|
|
539
643
|
|
|
540
644
|
const turns = h('div', 'turns');
|
|
541
645
|
const runs = [...c.runs].reverse();
|
|
542
646
|
if (!runs.length) {
|
|
543
647
|
const e = h('div', 'empty');
|
|
544
|
-
e.append(h('h3', null,
|
|
648
|
+
e.append(h('h3', null, L.noRuns));
|
|
545
649
|
turns.append(e);
|
|
546
650
|
}
|
|
547
651
|
for (const r of runs) {
|
|
@@ -555,18 +659,18 @@ button { font: inherit; color: inherit; }
|
|
|
555
659
|
head.append(chev());
|
|
556
660
|
head.append(h('span', 'tag ' + r.status, RUN[r.status] || r.status));
|
|
557
661
|
if (r.via && !(r.status === 'queued' && r.via === 'queue')) head.append(h('span', 'tag ghost', VIA[r.via] || r.via));
|
|
558
|
-
head.append(h('span', 'rid', (r.turn ?
|
|
559
|
-
if (r.escalations) head.append(h('span', 'tag warn',
|
|
560
|
-
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)));
|
|
561
665
|
const right = h('span', 'right');
|
|
562
666
|
const tools = r.items.filter((x) => x.kind === 'tool').length;
|
|
563
667
|
const n = r.tool_count != null ? r.tool_count : tools;
|
|
564
|
-
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);
|
|
565
669
|
const t0 = t(r.started_at);
|
|
566
670
|
const total = r.duration_ms != null ? r.duration_ms : r.status === 'running' && isFinite(t0) ? Date.now() - t0 : null;
|
|
567
671
|
if (r.tokens) {
|
|
568
672
|
const tk = h('span', 'tok-run');
|
|
569
|
-
tk.title =
|
|
673
|
+
tk.title = L.tokRunTitle(tin(r.tokens), r.tokens.cache_read, r.tokens.output);
|
|
570
674
|
tk.append(h('b', null, ftok(tin(r.tokens))), '↑ ', h('b', null, ftok(r.tokens.output)), '↓');
|
|
571
675
|
right.append(tk);
|
|
572
676
|
}
|
|
@@ -585,7 +689,7 @@ button { font: inherit; color: inherit; }
|
|
|
585
689
|
const spans = h('div', 'spans');
|
|
586
690
|
spans.dataset.key = key;
|
|
587
691
|
spans.tabIndex = 0; // 键盘也能滚(方向键 / PageDown)
|
|
588
|
-
spans.setAttribute('aria-label',
|
|
692
|
+
spans.setAttribute('aria-label', L.toolList);
|
|
589
693
|
for (const it of r.items) spans.append(spanRow(it, r, t0, total, c.cwd));
|
|
590
694
|
boxEl.append(spans, h('span', 'spans-count'));
|
|
591
695
|
body.append(boxEl);
|
|
@@ -596,7 +700,7 @@ button { font: inherit; color: inherit; }
|
|
|
596
700
|
turns.append(turn);
|
|
597
701
|
}
|
|
598
702
|
left.append(turns);
|
|
599
|
-
left.append(h('div', 'foot',
|
|
703
|
+
left.append(h('div', 'foot', L.foot));
|
|
600
704
|
cols.append(left, inspector(c));
|
|
601
705
|
wrap.append(cols);
|
|
602
706
|
main.append(wrap);
|
|
@@ -605,26 +709,26 @@ button { font: inherit; color: inherit; }
|
|
|
605
709
|
/** Token 面板:会话累计(DSH 记录)、构成、上下文窗口、每回合用量。 */
|
|
606
710
|
function tokenPanel(c) {
|
|
607
711
|
const p = h('section', 'panel');
|
|
608
|
-
p.append(h('h3', null,
|
|
712
|
+
p.append(h('h3', null, L.tokTitle));
|
|
609
713
|
const k = c.tokens;
|
|
610
714
|
if (!k) {
|
|
611
|
-
p.append(h('div', 'muted',
|
|
715
|
+
p.append(h('div', 'muted', L.tokNoData));
|
|
612
716
|
return p;
|
|
613
717
|
}
|
|
614
718
|
const big = h('div', 'tok-big');
|
|
615
719
|
const cell = (v, label) => { const d = h('div'); d.append(h('b', null, v), h('span', null, label)); big.append(d); };
|
|
616
|
-
cell(ftok(tin(k)),
|
|
617
|
-
cell(ftok(k.output),
|
|
720
|
+
cell(ftok(tin(k)), L.tIn);
|
|
721
|
+
cell(ftok(k.output), L.tOut);
|
|
618
722
|
const hr = hit(k);
|
|
619
|
-
cell(hr == null ? '—' : Math.round(hr * 100) + '%',
|
|
723
|
+
cell(hr == null ? '—' : Math.round(hr * 100) + '%', L.tHit);
|
|
620
724
|
p.append(big);
|
|
621
725
|
|
|
622
726
|
// 构成:未命中缓存的输入 / 缓存读 / 缓存写 / 输出
|
|
623
727
|
const parts = [
|
|
624
|
-
['c-unc',
|
|
625
|
-
['c-read',
|
|
626
|
-
['c-write',
|
|
627
|
-
['c-out',
|
|
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],
|
|
628
732
|
];
|
|
629
733
|
const sum = parts.reduce((n, x) => n + x[2], 0) || 1;
|
|
630
734
|
const stack = h('div', 'stack');
|
|
@@ -649,7 +753,7 @@ button { font: inherit; color: inherit; }
|
|
|
649
753
|
if (c.context && c.context.window) {
|
|
650
754
|
const pct = Math.min(100, (c.context.used / c.context.window) * 100);
|
|
651
755
|
const hh = h('div', 'ctx-h');
|
|
652
|
-
hh.append(h('span', null,
|
|
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))));
|
|
653
757
|
p.append(hh);
|
|
654
758
|
const bar = h('div', 'ctx' + (pct > 85 ? ' crit' : pct > 60 ? ' hi' : ''));
|
|
655
759
|
const b = h('b'); b.style.width = Math.max(1, pct) + '%'; bar.append(b);
|
|
@@ -661,23 +765,24 @@ button { font: inherit; color: inherit; }
|
|
|
661
765
|
const per = runs.map((r) => tin(r.tokens) + r.tokens.output);
|
|
662
766
|
const max = Math.max(...per) || 1;
|
|
663
767
|
const sh = h('div', 'spark-h');
|
|
664
|
-
sh.append(h('span', null,
|
|
768
|
+
sh.append(h('span', null, L.perRun), h('span', null, L.lastN(runs.length, ftok(max))));
|
|
665
769
|
p.append(sh);
|
|
666
770
|
const sp = h('div', 'spark');
|
|
667
771
|
runs.forEach((r, i) => {
|
|
668
772
|
const bar = h('i', r.status === 'running' ? 'cur' : '');
|
|
669
773
|
bar.style.height = Math.max(6, (per[i] / max) * 100) + '%';
|
|
670
|
-
bar.title = (r.turn ?
|
|
774
|
+
bar.title = (r.turn ? L.turnN(r.turn) : '') + r.run + L.barTail(ftok(tin(r.tokens)), ftok(r.tokens.output));
|
|
671
775
|
sp.append(bar);
|
|
672
776
|
});
|
|
673
777
|
p.append(sp);
|
|
674
778
|
}
|
|
675
|
-
p.append(h('div', 'foot',
|
|
779
|
+
p.append(h('div', 'foot', L.tokFoot));
|
|
676
780
|
return p;
|
|
677
781
|
}
|
|
678
782
|
|
|
679
783
|
/** 读写类工具:用来从输入预览里认出"涉及的文件"。 */
|
|
680
|
-
|
|
784
|
+
// 只认真正读文件的工具;grep / glob / search / ls 的输入是目录或模式,不算"涉及的文件"。
|
|
785
|
+
const READ_TOOLS = /^(read|view|cat|open)/i;
|
|
681
786
|
const WRITE_TOOLS = /^(write|edit|create|patch|apply|replace|delete|move|rename|mkdir)/i;
|
|
682
787
|
const looksLikePath = (s) => !!s && !/\s/.test(s) && (/[\\/]/.test(s) || /\.[A-Za-z0-9]{1,8}$/.test(s));
|
|
683
788
|
|
|
@@ -695,19 +800,19 @@ button { font: inherit; color: inherit; }
|
|
|
695
800
|
const esc = approvals.filter((x) => x.escalation).length;
|
|
696
801
|
|
|
697
802
|
const ov = h('section', 'panel');
|
|
698
|
-
ov.append(h('h3', null,
|
|
803
|
+
ov.append(h('h3', null, L.overview));
|
|
699
804
|
const kpis = h('div', 'kpis');
|
|
700
805
|
const kpi = (v, label, cls) => {
|
|
701
806
|
const k = h('div', 'kpi ' + (v && v !== '0' ? cls || '' : 'zero'));
|
|
702
807
|
k.append(h('b', null, String(v)), h('span', null, label));
|
|
703
808
|
kpis.append(k);
|
|
704
809
|
};
|
|
705
|
-
kpi(c.runs.length,
|
|
706
|
-
kpi(tools.length,
|
|
707
|
-
kpi(failed,
|
|
708
|
-
kpi(toolMs ? dur(toolMs) : '0',
|
|
709
|
-
kpi(approvals.length,
|
|
710
|
-
kpi(esc,
|
|
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');
|
|
711
816
|
ov.append(kpis);
|
|
712
817
|
box.append(ov);
|
|
713
818
|
box.append(tokenPanel(c));
|
|
@@ -720,8 +825,8 @@ button { font: inherit; color: inherit; }
|
|
|
720
825
|
byName.set(k, e);
|
|
721
826
|
}
|
|
722
827
|
const dist = h('section', 'panel');
|
|
723
|
-
dist.append(h('h3', null,
|
|
724
|
-
if (!byName.size) dist.append(h('div', 'muted',
|
|
828
|
+
dist.append(h('h3', null, L.toolDist));
|
|
829
|
+
if (!byName.size) dist.append(h('div', 'muted', L.noTools));
|
|
725
830
|
else {
|
|
726
831
|
const rows = [...byName.entries()].sort((a, b) => b[1].n - a[1].n || b[1].ms - a[1].ms);
|
|
727
832
|
const max = rows[0][1].n;
|
|
@@ -729,7 +834,7 @@ button { font: inherit; color: inherit; }
|
|
|
729
834
|
for (const [name, e] of rows.slice(0, 8)) {
|
|
730
835
|
const row = h('div', 'dist-row');
|
|
731
836
|
row.append(h('span', 'n', name));
|
|
732
|
-
row.append(h('span', 'v', '×' + e.n + (e.ms ? ' · ' + dur(e.ms) : '') + (e.fail ?
|
|
837
|
+
row.append(h('span', 'v', '×' + e.n + (e.ms ? ' · ' + dur(e.ms) : '') + (e.fail ? L.failN(e.fail) : '')));
|
|
733
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);
|
|
734
839
|
list.append(row);
|
|
735
840
|
}
|
|
@@ -749,13 +854,13 @@ button { font: inherit; color: inherit; }
|
|
|
749
854
|
files.set(key, prev === 'w' || op === 'w' ? 'w' : 'r');
|
|
750
855
|
}
|
|
751
856
|
const fp = h('section', 'panel');
|
|
752
|
-
fp.append(h('h3', null,
|
|
753
|
-
if (!files.size) fp.append(h('div', 'muted',
|
|
857
|
+
fp.append(h('h3', null, L.files));
|
|
858
|
+
if (!files.size) fp.append(h('div', 'muted', L.noFiles));
|
|
754
859
|
else {
|
|
755
860
|
const ul = h('ul', 'files');
|
|
756
861
|
for (const [p, op] of [...files.entries()].slice(-14).reverse()) {
|
|
757
862
|
const li = h('li');
|
|
758
|
-
li.append(h('span', 'op ' + op, op === 'w' ?
|
|
863
|
+
li.append(h('span', 'op ' + op, op === 'w' ? L.opW : L.opR));
|
|
759
864
|
const ps = h('span', 'p'); const bdi = document.createElement('bdi'); bdi.textContent = p; ps.append(bdi); ps.title = p;
|
|
760
865
|
li.append(ps);
|
|
761
866
|
ul.append(li);
|
|
@@ -766,7 +871,7 @@ button { font: inherit; color: inherit; }
|
|
|
766
871
|
|
|
767
872
|
if (approvals.length) {
|
|
768
873
|
const ap = h('section', 'panel');
|
|
769
|
-
ap.append(h('h3', null,
|
|
874
|
+
ap.append(h('h3', null, L.approvalsTitle));
|
|
770
875
|
const list = h('div', 'aps');
|
|
771
876
|
for (const a of approvals.slice(-6).reverse()) {
|
|
772
877
|
const d = h('div', 'ap');
|
|
@@ -794,7 +899,7 @@ button { font: inherit; color: inherit; }
|
|
|
794
899
|
box.classList.toggle('more-up', spans.scrollTop > 2);
|
|
795
900
|
box.classList.toggle('more-down', spans.scrollTop < max - 2);
|
|
796
901
|
const hiddenBelow = Math.max(0, Math.round((max - spans.scrollTop) / 32));
|
|
797
|
-
box.lastElementChild.textContent = hiddenBelow ?
|
|
902
|
+
box.lastElementChild.textContent = hiddenBelow ? L.moreBelow(hiddenBelow) : '';
|
|
798
903
|
}
|
|
799
904
|
function restoreScroll() {
|
|
800
905
|
for (const { spans, box, key, running } of pendingScroll) {
|
|
@@ -830,6 +935,13 @@ button { font: inherit; color: inherit; }
|
|
|
830
935
|
}
|
|
831
936
|
|
|
832
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);
|
|
833
945
|
|
|
834
946
|
function setConn(on, text) {
|
|
835
947
|
const c = $('conn');
|
|
@@ -837,9 +949,9 @@ button { font: inherit; color: inherit; }
|
|
|
837
949
|
c.lastElementChild.textContent = text;
|
|
838
950
|
}
|
|
839
951
|
const es = new EventSource('/events?t=' + encodeURIComponent(token));
|
|
840
|
-
es.onopen = () => setConn(true,
|
|
952
|
+
es.onopen = () => { connected = true; setConn(true, L.live); };
|
|
841
953
|
es.onmessage = (e) => { try { view = JSON.parse(e.data); } catch { return; } render(); };
|
|
842
|
-
es.onerror = () => setConn(false,
|
|
954
|
+
es.onerror = () => { connected = false; setConn(false, L.reconnecting); };
|
|
843
955
|
setInterval(render, 15000);
|
|
844
956
|
})();
|
|
845
957
|
</script>
|