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.
@@ -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>连接中</span></span></div>
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" placeholder="筛选标题、id、目录" aria-label="筛选会话">
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" aria-label="会话"></nav>
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
- const STATE = { running: '运行中', queued: '排队中', idle: '空闲', closed: '已释放' };
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, '运行中', 'hot');
354
- stat(s.queued, '排队', 'warn');
355
- stat(s.pending_approvals, '待审批', 'warn');
356
- stat(s.escalations, '提权', 'warn');
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 = '全部会话 token:输入 ' + tin(s.tokens).toLocaleString() + '(缓存读 ' + s.tokens.cache_read.toLocaleString() + ')· 输出 ' + s.tokens.output.toLocaleString();
360
- d.append(h('b', null, ftok(tin(s.tokens) + s.tokens.output)), h('span', null, 'token · 出 ' + ftok(s.tokens.output)));
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 || '无标题 · ' + short(c.id)));
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('用 ', h('code', null, 'dsh_send'), ' 派一次活,调用链会出现在这里。');
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 ? '等待' : (MODE[it.mode] ? '' : '')));
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' ? '中断' : '插话' + (it.mode ? ' · ' + it.mode : '')));
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) + ' 起 · ' + dur(end - start);
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('这是只读视图。服务端写入 ', h('code', null, 'live/'), ' 事件流后,这里按 客户端 → 会话 → 回合 → 工具 展开调用链。');
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('状态', STATE[c.state] || c.state);
526
- fact('客户端', (c.client || '未知') + ' · pid ' + (c.writer ? String(c.writer).split(':')[0] : '?') + (c.writer_alive ? '' : '(已退出)'));
527
- fact('权限', [c.permission, c.on_approval].filter(Boolean).join(' · '));
528
- fact('模型', c.model);
529
- fact('会话 id', short(c.id), 'mono', c.id);
530
- fact('目录', c.cwd, 'mono path');
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, '调用链'), h('span', null, c.runs.length + ' 个回合 · 最新在上'));
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 ? '第 ' + r.turn + ' 轮 · ' : '') + r.run));
559
- if (r.escalations) head.append(h('span', 'tag warn', '⚠ ' + r.escalations + ' 次提权'));
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)), ' 个工具'); right.append(rb);
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 = '本回合 token:输入 ' + tin(r.tokens) + '(缓存读 ' + r.tokens.cache_read + ')· 输出 ' + r.tokens.output;
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', '只读视图 · 数据来自本机 live 事件流 · 思考内容从不写入 · 点击回合标题折叠'));
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, 'Token 消耗'));
712
+ p.append(h('h3', null, L.tokTitle));
609
713
  const k = c.tokens;
610
714
  if (!k) {
611
- p.append(h('div', 'muted', '暂无数据:派一次活之后,这里会实时显示本会话的 token 用量。'));
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', '未缓存输入', k.input],
625
- ['c-read', '缓存读', k.cache_read],
626
- ['c-write', '缓存写', k.cache_write],
627
- ['c-out', '输出', k.output],
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, '上下文窗口'), h('span', null, ftok(c.context.used) + ' / ' + ftok(c.context.window) + '(' + (pct < 1 ? pct.toFixed(1) : Math.round(pct)) + '%)'));
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, '每回合用量'), h('span', null, '最近 ' + runs.length + ' 轮 · 最高 ' + ftok(max)));
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 ? '第 ' + r.turn + ' 轮 · ' : '') + r.run + ':输入 ' + ftok(tin(r.tokens)) + ' · 输出 ' + ftok(r.tokens.output);
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', '数字来自 DSH 自己的会话记录(累计,含 live 之前的回合)。'));
779
+ p.append(h('div', 'foot', L.tokFoot));
676
780
  return p;
677
781
  }
678
782
 
679
783
  /** 读写类工具:用来从输入预览里认出"涉及的文件"。 */
680
- const READ_TOOLS = /^(read|view|cat|open|glob|grep|search|list|ls)/i;
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, '失败', 'bad');
708
- kpi(toolMs ? dur(toolMs) : '0', '工具耗时');
709
- kpi(approvals.length, '审批', 'warn');
710
- kpi(esc, '提权', 'warn');
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 ? ' · ' + 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 ? '↓ ' + 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>