@deepseek-harness-tui/dsh-tui 0.10.0-beta.2 → 0.10.0-beta.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.
Files changed (35) hide show
  1. package/lib/types/commands.d.ts.map +1 -1
  2. package/lib/types/commands.js +1 -0
  3. package/lib/types/components/Chat/JobCard.d.ts +23 -0
  4. package/lib/types/components/Chat/JobCard.d.ts.map +1 -0
  5. package/lib/types/components/Chat/JobCard.js +86 -0
  6. package/lib/types/components/JobsPanel.d.ts +17 -0
  7. package/lib/types/components/JobsPanel.d.ts.map +1 -0
  8. package/lib/types/components/JobsPanel.js +111 -0
  9. package/lib/types/components/MessageList.d.ts +3 -1
  10. package/lib/types/components/MessageList.d.ts.map +1 -1
  11. package/lib/types/components/MessageList.js +35 -16
  12. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
  13. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  14. package/lib/types/components/messages/AssistantToolUseMessage.js +7 -5
  15. package/lib/types/dsh-adapter/channel.d.ts +36 -1
  16. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  17. package/lib/types/dsh-adapter/channel.js +187 -0
  18. package/lib/types/dsh-adapter/jobs.d.ts +145 -0
  19. package/lib/types/dsh-adapter/jobs.d.ts.map +1 -0
  20. package/lib/types/dsh-adapter/jobs.js +202 -0
  21. package/lib/types/i18n.d.ts +95 -0
  22. package/lib/types/i18n.d.ts.map +1 -1
  23. package/lib/types/i18n.js +25 -0
  24. package/lib/types/ink/ink.d.ts.map +1 -1
  25. package/lib/types/ink/ink.js +12 -5
  26. package/lib/types/ink/selection.d.ts +31 -1
  27. package/lib/types/ink/selection.d.ts.map +1 -1
  28. package/lib/types/ink/selection.js +48 -6
  29. package/lib/types/screens/Chat.d.ts.map +1 -1
  30. package/lib/types/screens/Chat.js +19 -1
  31. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  32. package/lib/types/screens/StatusLine.js +27 -0
  33. package/lib/types/tips.d.ts.map +1 -1
  34. package/lib/types/tips.js +6 -0
  35. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAElF,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,2EAA2E;AAC3E,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,KAAK,SAAS,qBAAqB,EAAE,CAAA;AAEpG;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,YAAY,EA2DxC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAElD,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAEpD,CAAA;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAIhG;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIhD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,OAAO,CAKT;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,YAAY,EAAE,CAKhB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,SAAS,YAAY,EAAmB,EAC/C,QAAQ,GAAE,eAA0B,GACnC,iBAAiB,EAAE,CAuCrB"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAElF,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,2EAA2E;AAC3E,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,KAAK,SAAS,qBAAqB,EAAE,CAAA;AAEpG;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,YAAY,EA4DxC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAElD,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAEpD,CAAA;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAIhG;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIhD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,OAAO,CAKT;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,YAAY,EAAE,CAKhB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,SAAS,YAAY,EAAmB,EAC/C,QAAQ,GAAE,eAA0B,GACnC,iBAAiB,EAAE,CAuCrB"}
@@ -43,6 +43,7 @@ export const LOCAL_COMMANDS = [
43
43
  { name: 'doctor', description: 'Run environment checks' },
44
44
  { name: 'init', description: 'Create AGENTS.md in the working directory' },
45
45
  { name: 'agents', description: 'Show subagents of this session' },
46
+ { name: 'jobs', description: 'Show background jobs of this session' },
46
47
  // Model / display
47
48
  { name: 'activity', description: 'Switch the working-activity indicator preset' },
48
49
  { name: 'preset', description: 'Switch the agent preset (including Liangshen mode)' },
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import type { JobRow } from '../../dsh-adapter/channel.js';
3
+ /**
4
+ * Live background-job card embedded in the transcript (`kind: 'job'`),
5
+ * sibling of the subagent card: header (id · kind · label · elapsed ·
6
+ * status) plus a bounded output waterfall (up to three rows) while the job
7
+ * is live — and only when mirrored output exists: background jobs are
8
+ * usually silent, so an outputless card is just its header line, never a
9
+ * row of empty gutters. Settled jobs fold to the header line alone (a
10
+ * failed/killed job keeps one detail line); the `/jobs` panel holds the
11
+ * fuller view the card clicks to.
12
+ *
13
+ * The waterfall is MIRRORED, never polled: the harness job registry's read
14
+ * is consuming and reserved for the owning agent, so the card shows the
15
+ * tail of the agent's own job_output results as they stream through the
16
+ * transcript.
17
+ */
18
+ export declare function JobCard({ job, addMargin, onClick }: {
19
+ job: JobRow;
20
+ addMargin: boolean;
21
+ onClick?(): void;
22
+ }): React.ReactNode;
23
+ //# sourceMappingURL=JobCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobCard.d.ts","sourceRoot":"","sources":["../../../../src/components/Chat/JobCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAA;AAkD1D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;IACnD,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,IAAI,IAAI,CAAA;CACjB,GAAG,KAAK,CAAC,SAAS,CAuDlB"}
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useAnimationFrame, useTerminalSize } from '../../ui.js';
4
+ import { formatJobDuration } from '../../dsh-adapter/jobs.js';
5
+ import { t } from '../../i18n.js';
6
+ import { stringWidth } from '../../ink/stringWidth.js';
7
+ import { isMinimalMode } from '../../minimalMode.js';
8
+ /** The waterfall window mirrors the subagent card: a constant-height region. */
9
+ const WATERFALL_ROWS = 3;
10
+ /** Card left padding + the `│ ` gutter prefix. */
11
+ const WATERFALL_GUTTER = 4;
12
+ /** Static status marker — deliberately NOT the animated activity-indicator
13
+ * preset: a background job is parked work, and reusing the main spinner
14
+ * language for every card reads as clutter. ● = live background work
15
+ * (echoing the status-line chip), ✓/✗ for terminal states. NOTE: no ⚙ —
16
+ * U+2699 is East-Asian Ambiguous: ink measures it 1 cell while CJK
17
+ * terminal fonts paint it 2, so the following text overlaps the glyph. */
18
+ function statusInfo(status) {
19
+ const minimal = isMinimalMode();
20
+ switch (status) {
21
+ case 'completed':
22
+ return { glyph: '✓', label: t('jobs-status-completed'), color: minimal ? undefined : 'success' };
23
+ case 'failed':
24
+ return { glyph: '✗', label: t('jobs-status-failed'), color: minimal ? undefined : 'error' };
25
+ case 'killed':
26
+ return { glyph: '✗', label: t('jobs-status-killed'), color: minimal ? undefined : 'error' };
27
+ case 'stopping':
28
+ return { glyph: '●', label: t('jobs-status-stopping'), color: minimal ? undefined : 'warning' };
29
+ default:
30
+ return { glyph: '●', label: t('jobs-status-running'), color: minimal ? undefined : 'warning' };
31
+ }
32
+ }
33
+ /** Hard single-line clip by display width — a wrapped waterfall row would
34
+ * break the constant-height window. */
35
+ function clipLine(text, maxWidth) {
36
+ if (maxWidth <= 1)
37
+ return '';
38
+ let width = 0;
39
+ let index = 0;
40
+ while (index < text.length) {
41
+ const next = text.codePointAt(index);
42
+ const char = String.fromCodePoint(next);
43
+ const charWidth = stringWidth(char);
44
+ if (width + charWidth > maxWidth - 1)
45
+ break;
46
+ width += charWidth;
47
+ index += char.length;
48
+ }
49
+ return index < text.length ? `${text.slice(0, index)}…` : text;
50
+ }
51
+ /**
52
+ * Live background-job card embedded in the transcript (`kind: 'job'`),
53
+ * sibling of the subagent card: header (id · kind · label · elapsed ·
54
+ * status) plus a bounded output waterfall (up to three rows) while the job
55
+ * is live — and only when mirrored output exists: background jobs are
56
+ * usually silent, so an outputless card is just its header line, never a
57
+ * row of empty gutters. Settled jobs fold to the header line alone (a
58
+ * failed/killed job keeps one detail line); the `/jobs` panel holds the
59
+ * fuller view the card clicks to.
60
+ *
61
+ * The waterfall is MIRRORED, never polled: the harness job registry's read
62
+ * is consuming and reserved for the owning agent, so the card shows the
63
+ * tail of the agent's own job_output results as they stream through the
64
+ * transcript.
65
+ */
66
+ export function JobCard({ job, addMargin, onClick }) {
67
+ const settled = job.status === 'completed' || job.status === 'failed' || job.status === 'killed';
68
+ // 动画订阅仅限存活卡片:settled 后退订共享 clock(同 SubagentMessage 的
69
+ // 约定)。1s tick 只驱动运行时长跳动——状态标是静态的(见 statusInfo)。
70
+ const [viewportRef] = useAnimationFrame(settled ? null : 1000);
71
+ const { columns } = useTerminalSize();
72
+ const info = statusInfo(job.status);
73
+ const [hovered, setHovered] = React.useState(false);
74
+ const clickable = onClick !== undefined;
75
+ const rowWidth = Math.max(20, (columns ?? 80) - WATERFALL_GUTTER);
76
+ const activity = settled ? [] : job.outputLines.slice(-WATERFALL_ROWS);
77
+ // A settled job's terminal detail ('exit code: 0') rides the header; a
78
+ // failed/killed one also keeps it as the explanatory tail line.
79
+ const headerDetail = job.detail !== undefined && job.detail !== '' ? job.detail : undefined;
80
+ // 点击打开 /jobs 面板;hover 不刷整行背景(转录视觉保持安静),只把
81
+ // 状态 glyph 提亮为品牌色作为可点指示。无外层缩进:任务卡是上方工具
82
+ // 调用(run_in_background 卡)的延续,与工具卡通栏左对齐;子代理卡才
83
+ // 是嵌套子实体、保留缩进。瀑布的 ` │ ` 槽自带两格,正好与工具卡正文
84
+ // 的 ` ⎿ ` 槽位一致。
85
+ return _jsxs(Box, { flexDirection: "column", marginTop: addMargin ? 1 : 0, ref: viewportRef, onClick: onClick, onMouseEnter: clickable ? () => setHovered(true) : undefined, onMouseLeave: clickable ? () => setHovered(false) : undefined, children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: hovered && clickable ? 'claude' : info.color, children: info.glyph }), _jsx(Text, { bold: true, color: hovered && clickable ? 'claude' : undefined, children: `${t('jobs-card-prefix')}${job.id}` }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: job.kind }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { children: clipLine(job.label, Math.max(10, rowWidth - 30)) }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: formatJobDuration(job) }), headerDetail !== undefined && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: headerDetail })] }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: info.color, children: info.label })] }), !settled && activity.length > 0 && activity.map((line, index) => (_jsx(Text, { dimColor: true, wrap: "truncate", children: ` │ ${clipLine(line, rowWidth)}` }, `${job.id}-wf-${index}`))), settled && job.status !== 'completed' && headerDetail !== undefined && (_jsx(Text, { dimColor: true, wrap: "truncate", children: ` └ ${clipLine(headerDetail, rowWidth)}` }))] });
86
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { type BackgroundJobState } from '../dsh-adapter/jobs.js';
3
+ export interface JobsPanelProps {
4
+ jobs: readonly BackgroundJobState[];
5
+ onClose: () => void;
6
+ /** Kill the focused live job (`job_kill` with the session's authority). */
7
+ onKill: (id: string) => void;
8
+ }
9
+ /**
10
+ * `/jobs` overlay panel — every background job of the current session with
11
+ * live status, elapsed/total duration and terminal detail (exit code).
12
+ * Keyboard: ↑/↓ move, k kills the focused live job, Esc closes; the focused
13
+ * row expands a detail block (full label, start/finish times, mirrored
14
+ * output tail). The panel is the deep view behind the transcript job cards.
15
+ */
16
+ export declare function JobsPanel({ jobs, onClose, onKill }: JobsPanelProps): React.ReactNode;
17
+ //# sourceMappingURL=JobsPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobsPanel.d.ts","sourceRoot":"","sources":["../../../src/components/JobsPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAqB,KAAK,kBAAkB,EAA4B,MAAM,wBAAwB,CAAA;AAS7G,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACnC,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,2EAA2E;IAC3E,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B;AAkGD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,cAAc,GAAG,KAAK,CAAC,SAAS,CA2FpF"}
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput, ScrollBox, useTerminalSize, useAnimationFrame } from '../ui.js';
4
+ import { formatJobDuration } from '../dsh-adapter/jobs.js';
5
+ import { t } from '../i18n.js';
6
+ import { Divider } from './design-system/Divider.js';
7
+ import { ExitButton } from './SubagentDashboard.js';
8
+ import { isPlainReturnInput } from '../utils/modifiers.js';
9
+ import { isMinimalMode } from '../minimalMode.js';
10
+ import { stringWidth } from '../ink/stringWidth.js';
11
+ function statusInfo(status) {
12
+ const minimal = isMinimalMode();
13
+ switch (status) {
14
+ case 'completed':
15
+ return { glyph: minimal ? '✓' : '●', label: t('jobs-status-completed'), color: minimal ? undefined : 'success' };
16
+ case 'failed':
17
+ return { glyph: minimal ? '×' : '●', label: t('jobs-status-failed'), color: minimal ? undefined : 'error' };
18
+ case 'killed':
19
+ return { glyph: minimal ? '×' : '●', label: t('jobs-status-killed'), color: minimal ? undefined : 'error' };
20
+ case 'stopping':
21
+ return { glyph: minimal ? '·' : '●', label: t('jobs-status-stopping'), color: minimal ? undefined : 'warning' };
22
+ default:
23
+ return { glyph: minimal ? '·' : '●', label: t('jobs-status-running'), color: minimal ? undefined : 'warning' };
24
+ }
25
+ }
26
+ /** Hard single-line clip by display width (shared rule with the job card). */
27
+ function clipLine(text, maxWidth) {
28
+ if (maxWidth <= 1)
29
+ return '';
30
+ let width = 0;
31
+ let index = 0;
32
+ while (index < text.length) {
33
+ const next = text.codePointAt(index);
34
+ const char = String.fromCodePoint(next);
35
+ const charWidth = stringWidth(char);
36
+ if (width + charWidth > maxWidth - 1)
37
+ break;
38
+ width += charWidth;
39
+ index += char.length;
40
+ }
41
+ return index < text.length ? `${text.slice(0, index)}…` : text;
42
+ }
43
+ function JobRowLine({ job, focused }) {
44
+ const { columns } = useTerminalSize();
45
+ const info = statusInfo(job.status);
46
+ const duration = formatJobDuration(job);
47
+ const detail = job.detail !== undefined && job.detail !== '' ? job.detail : undefined;
48
+ // Reserve: glyph(2) id(~9) kind(~7) duration(~7) status(~6) separators(5×2)
49
+ // — the label takes the rest and hard-clips instead of wrapping.
50
+ const labelWidth = Math.max(10, (columns ?? 80) - 46);
51
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: focused ? 'claude' : undefined, children: focused ? '❯' : ' ' }), _jsx(Text, { color: info.color, children: info.glyph }), _jsx(Text, { bold: focused, color: focused ? 'claude' : undefined, children: job.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: job.kind }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: focused, children: clipLine(job.label, labelWidth) }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: duration }), detail !== undefined && _jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: detail })] }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: info.color, children: info.label })] }), focused && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: ` ${t('jobs-card-prefix')}${clipLine(job.label, Math.max(10, labelWidth + 24))}` }), job.command !== undefined && (_jsx(Text, { dimColor: true, wrap: "truncate", children: ` ${t('jobs-panel-command')} ${clipLine(job.command, Math.max(10, labelWidth + 24))}` })), _jsxs(Text, { dimColor: true, wrap: "truncate", children: [` ${t('jobs-panel-started')} ${timeOf(job.startedAt)}`, job.finishedAt !== undefined ? ` · ${t('jobs-panel-finished')} ${timeOf(job.finishedAt)}` : '', job.lastOutputAt !== undefined ? ` · ${t('jobs-panel-output-at')} ${timeOf(job.lastOutputAt)}` : ''] }), job.outputLines.length > 0 ? (job.outputLines.map((line, index) => (_jsx(Text, { dimColor: true, wrap: "truncate", children: ` │ ${clipLine(line, Math.max(10, labelWidth + 24))}` }, `${job.id}-detail-${index}`)))) : (_jsx(Text, { dimColor: true, wrap: "truncate", children: ` └ ${t('jobs-panel-no-output-yet')}` }))] }))] }));
52
+ }
53
+ /** HH:MM:SS wall-clock of an epoch ms value (locale-independent). */
54
+ function timeOf(ms) {
55
+ const date = new Date(ms);
56
+ const pad = (value) => String(value).padStart(2, '0');
57
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
58
+ }
59
+ /**
60
+ * `/jobs` overlay panel — every background job of the current session with
61
+ * live status, elapsed/total duration and terminal detail (exit code).
62
+ * Keyboard: ↑/↓ move, k kills the focused live job, Esc closes; the focused
63
+ * row expands a detail block (full label, start/finish times, mirrored
64
+ * output tail). The panel is the deep view behind the transcript job cards.
65
+ */
66
+ export function JobsPanel({ jobs, onClose, onKill }) {
67
+ const [focusIndex, setFocusIndex] = React.useState(0);
68
+ const scrollRef = React.useRef(null);
69
+ const { rows } = useTerminalSize();
70
+ // 1s tick keeps live durations counting while the panel is open.
71
+ const [clockRef] = useAnimationFrame(1000);
72
+ const focus = Math.min(focusIndex, Math.max(0, jobs.length - 1));
73
+ useInput((input, key, event) => {
74
+ if (key.escape || (key.ctrl && input === 'c')) {
75
+ event.stopImmediatePropagation();
76
+ onClose();
77
+ return;
78
+ }
79
+ if (key.upArrow) {
80
+ event.stopImmediatePropagation();
81
+ setFocusIndex(i => Math.max(0, i - 1));
82
+ scrollRef.current?.scrollBy(-1);
83
+ return;
84
+ }
85
+ if (key.downArrow) {
86
+ event.stopImmediatePropagation();
87
+ setFocusIndex(i => Math.min(jobs.length - 1, i + 1));
88
+ scrollRef.current?.scrollBy(1);
89
+ return;
90
+ }
91
+ if (input === 'k') {
92
+ const selected = jobs[focus];
93
+ if (selected !== undefined && (selected.status === 'running' || selected.status === 'stopping')) {
94
+ event.stopImmediatePropagation();
95
+ onKill(selected.id);
96
+ }
97
+ return;
98
+ }
99
+ // Enter on a live job does nothing extra (the card/panel IS the view);
100
+ // keep the key consumed while the panel owns the keyboard.
101
+ if (isPlainReturnInput(input, key)) {
102
+ event.stopImmediatePropagation();
103
+ return;
104
+ }
105
+ event.stopImmediatePropagation();
106
+ });
107
+ const running = jobs.filter(job => job.status === 'running' || job.status === 'stopping').length;
108
+ const completed = jobs.filter(job => job.status === 'completed').length;
109
+ const failed = jobs.filter(job => job.status === 'failed' || job.status === 'killed').length;
110
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, ref: clockRef, children: [_jsx(Divider, { color: "claude", title: t('jobs-panel-title') }), _jsxs(Box, { flexDirection: "row", gap: 3, marginTop: 1, marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "claude", children: running }), _jsxs(Text, { dimColor: true, children: [" ", t('jobs-panel-count-running')] })] }), _jsxs(Text, { children: [_jsx(Text, { color: "success", children: completed }), _jsxs(Text, { dimColor: true, children: [" ", t('jobs-panel-count-completed')] })] }), failed > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: "error", children: failed }), _jsxs(Text, { dimColor: true, children: [" ", t('jobs-panel-count-failed')] })] })), _jsx(Box, { flexGrow: 1 }), _jsx(ExitButton, { onClick: onClose })] }), _jsx(Box, { flexDirection: "column", maxHeight: Math.max(10, rows - 10), marginTop: 1, children: _jsx(ScrollBox, { ref: scrollRef, flexDirection: "column", flexGrow: 1, children: jobs.length === 0 ? (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: Math.max(2, Math.floor((rows - 16) / 3)), children: [_jsx(Text, { dimColor: true, children: '○' }), _jsx(Text, { dimColor: true, children: t('jobs-panel-empty') }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('jobs-panel-empty-hint') }) })] })) : (jobs.map((job, index) => _jsx(JobRowLine, { job: job, focused: index === focus }, job.id))) }) }), _jsx(Divider, { color: "subtle", title: "" }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { dimColor: true, children: t('jobs-panel-hint') }) })] }));
111
+ }
@@ -4,7 +4,7 @@ import type { ChatRow } from '../dsh-adapter/channel.js';
4
4
  import type { DOMElement } from '../ink/dom.js';
5
5
  import { type TimelineSnapshot } from '../ink/timeline-rail.js';
6
6
  import type { ToolBackground } from '../tuiDisplayPrefs.js';
7
- export declare function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows, onToggleStreamView, model, diffLayout, thinkingFold, toolBackground, foldTerminalCommand, smoothStreaming, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible, historyPaintEnabled, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }: {
7
+ export declare function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows, onToggleStreamView, model, diffLayout, thinkingFold, toolBackground, foldTerminalCommand, smoothStreaming, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible, historyPaintEnabled, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenJobs, onOpenFile, }: {
8
8
  rows: readonly ChatRow[];
9
9
  expanded: boolean;
10
10
  expandedRows: ReadonlySet<number>;
@@ -88,6 +88,8 @@ export declare function MessageList({ rows, expanded, expandedRows, selectedId,
88
88
  failureHint?: string;
89
89
  /** 打开子代理详情场景(transcript 内点击子代理卡)。 */
90
90
  onOpenSubagent?: (agentId: string) => void;
91
+ /** 打开 /jobs 后台任务面板(transcript 内点击任务卡)。 */
92
+ onOpenJobs?: () => void;
91
93
  /** 点击工具卡内的文件路径(打开文件操作菜单)。 */
92
94
  onOpenFile?: (path: string) => void;
93
95
  }): React.JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ import { AssistantTextMessage } from './messages/AssistantTextMessage.js';
8
8
  import { AssistantThinkingMessage } from './messages/AssistantThinkingMessage.js';
9
9
  import { AssistantToolUseMessage } from './messages/AssistantToolUseMessage.js';
10
10
  import { SubagentMessage } from './Chat/SubagentMessage.js';
11
+ import { JobCard } from './Chat/JobCard.js';
11
12
  import { isMinimalMode } from '../minimalMode.js';
12
13
  import { noteFrameCause, noteListGeometry } from '../ink/geometry-trace.js';
13
14
  import { InterruptedByUser } from './InterruptedByUser.js';
@@ -153,7 +154,7 @@ function signatureParts(row, columns, expanded, expandedRows, streamViewToggledR
153
154
  }
154
155
  return signatureScratch;
155
156
  }
156
- export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows = NO_STREAM_VIEW_TOGGLED, onToggleStreamView = NOOP_TOGGLE_STREAM_VIEW, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', foldTerminalCommand = false, smoothStreaming = false, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, historyPaintEnabled = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }) {
157
+ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows = NO_STREAM_VIEW_TOGGLED, onToggleStreamView = NOOP_TOGGLE_STREAM_VIEW, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', foldTerminalCommand = false, smoothStreaming = false, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, historyPaintEnabled = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenJobs, onOpenFile, }) {
157
158
  const hiddenCount = rows.length - MAX_RENDERED_ROWS;
158
159
  // The thinking filter runs BEFORE virtualization so window indices line up.
159
160
  //
@@ -300,7 +301,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
300
301
  /** True when frame-budgeted history painting still has batches left
301
302
  * (main-screen open): the layout effect schedules the next slice. */
302
303
  const paintPendingRef = React.useRef(false);
303
- const paintQueuedRef = React.useRef(false);
304
+ const paintTimerRef = React.useRef(null);
304
305
  /** Persistent history-paint edge: how far batched painting has advanced
305
306
  * (index into visibleRows). -1 = not painting / reset (list head change:
306
307
  * rewind, new session, loadOlder — must repaint from scratch). */
@@ -316,9 +317,19 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
316
317
  paintedBaseRef.current = listHeadId;
317
318
  /** Content-space offset of visibleRows[0] (header + dividers), measured. */
318
319
  const baseRef = React.useRef(null);
319
- const measureQueuedRef = React.useRef(false);
320
+ const measureTimerRef = React.useRef(null);
320
321
  const [, setMeasureTick] = React.useState(0);
321
322
  const [, setScrollTick] = React.useState(0);
323
+ React.useEffect(() => () => {
324
+ if (measureTimerRef.current !== null) {
325
+ clearTimeout(measureTimerRef.current);
326
+ measureTimerRef.current = null;
327
+ }
328
+ if (paintTimerRef.current !== null) {
329
+ clearTimeout(paintTimerRef.current);
330
+ paintTimerRef.current = null;
331
+ }
332
+ }, []);
322
333
  // A width change reflows every row — all measurements are stale.
323
334
  const lastColumns = React.useRef(columns);
324
335
  if (lastColumns.current !== columns) {
@@ -828,15 +839,15 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
828
839
  scrollHandle.setClampBounds(min, bottomPad <= 0 ? undefined : Math.max(min, base + mountedBottom - viewport));
829
840
  }
830
841
  }
831
- if (changed && !measureQueuedRef.current) {
832
- // Layout corrections can cascade for many rows. Yield between commits
833
- // so React does not count the valid convergence as nested updates.
834
- measureQueuedRef.current = true;
835
- queueMicrotask(() => {
836
- measureQueuedRef.current = false;
842
+ if (changed && measureTimerRef.current === null) {
843
+ // Layout corrections can cascade for many rows. Yield to the next
844
+ // macrotask so React does not count the valid convergence as nested
845
+ // updates when a streaming/reveal commit is already in flight.
846
+ measureTimerRef.current = setTimeout(() => {
847
+ measureTimerRef.current = null;
837
848
  noteFrameCause('measure');
838
849
  setMeasureTick(t => t + 1);
839
- });
850
+ }, 0);
840
851
  }
841
852
  // History-paint continuation (main-screen open): more never-painted
842
853
  // batches remain — schedule the next slice on the macrotask queue so
@@ -845,10 +856,9 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
845
856
  // enough: each batch's mount+measure work is bounded (~2 viewports),
846
857
  // unlike the previous single-commit full mount that saturated the main
847
858
  // thread for seconds.
848
- if (paintPendingRef.current && !paintQueuedRef.current) {
849
- paintQueuedRef.current = true;
850
- setTimeout(() => {
851
- paintQueuedRef.current = false;
859
+ if (paintPendingRef.current && paintTimerRef.current === null) {
860
+ paintTimerRef.current = setTimeout(() => {
861
+ paintTimerRef.current = null;
852
862
  if (!paintPendingRef.current)
853
863
  return;
854
864
  noteFrameCause('measure');
@@ -873,6 +883,11 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
873
883
  const addMargin = margins.get(row.id) === true;
874
884
  const tool = row.tool;
875
885
  const subagent = row.kind === 'subagent' ? row.subagent : undefined;
886
+ const job = row.kind === 'job' ? row.job : undefined;
887
+ const revealVersion = smoothStreaming && row.kind === 'tool' && row.fresh === true &&
888
+ row.tool?.status === 'running' && row.tool.resultView === undefined
889
+ ? getRevealVersion()
890
+ : 0;
876
891
  // Smooth reveal feeds the SAME flattened text prop a chunk feeds,
877
892
  // and keeps the streaming layout alive until the reveal catches up
878
893
  // (settling mid-reveal must not snap — a one-shot non-streaming
@@ -893,7 +908,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
893
908
  else if (row.kind === 'reasoning' && smoothStreaming) {
894
909
  displayText = revealTextOf(`r${row.id}`, row.text, { enabled: true, active: displayStreaming });
895
910
  }
896
- return (_jsx(MemoRow, { rowId: row.id, kind: row.kind, text: displayText, textFull: row.kind === 'reasoning' ? row.text : undefined, executionTarget: row.executionTarget, streaming: displayStreaming, durationMs: row.durationMs, time: row.time, addMargin: addMargin, isSelected: selectedId === row.id, isExpanded: expandedRows.has(row.id), expanded: expanded, model: model, diffLayout: diffLayout, thinkingFold: thinkingFold, toolBackground: toolBackground, foldTerminalCommand: foldTerminalCommand, smoothStreaming: smoothStreaming, fresh: row.fresh === true, activityFrames: activityFrames, background: rowBackground(row.id), toolCallId: tool?.callId, toolName: tool?.name, toolArgsText: tool?.argsText, toolArgsFull: tool?.argsFull, toolStatus: tool?.status, toolResultText: tool?.resultText, toolResultFull: tool?.resultFull, toolErrorText: tool?.errorText, toolFootnote: failureHintRowId === row.id ? failureHint : undefined, toolCallView: tool?.callView, toolResultView: tool?.resultView, toolStartedAt: tool?.startedAt, toolDurationMs: tool?.durationMs, subagent: subagent, onToggleRow: onToggleRow, onToggleStreamView: onToggleStreamView, streamViewToggled: streamViewToggledRows.has(row.id), onOpenSubagent: onOpenSubagent, onOpenFile: onOpenFile, setRowRef: setRowRef }, row.id));
911
+ return (_jsx(MemoRow, { rowId: row.id, kind: row.kind, text: displayText, textFull: row.kind === 'reasoning' ? row.text : undefined, executionTarget: row.executionTarget, streaming: displayStreaming, durationMs: row.durationMs, time: row.time, addMargin: addMargin, isSelected: selectedId === row.id, isExpanded: expandedRows.has(row.id), expanded: expanded, model: model, diffLayout: diffLayout, thinkingFold: thinkingFold, toolBackground: toolBackground, foldTerminalCommand: foldTerminalCommand, smoothStreaming: smoothStreaming, fresh: row.fresh === true, revealVersion: revealVersion, activityFrames: activityFrames, background: rowBackground(row.id), toolCallId: tool?.callId, toolName: tool?.name, toolArgsText: tool?.argsText, toolArgsFull: tool?.argsFull, toolStatus: tool?.status, toolResultText: tool?.resultText, toolResultFull: tool?.resultFull, toolErrorText: tool?.errorText, toolFootnote: failureHintRowId === row.id ? failureHint : undefined, toolCallView: tool?.callView, toolResultView: tool?.resultView, toolStartedAt: tool?.startedAt, toolDurationMs: tool?.durationMs, subagent: subagent, job: job, onToggleRow: onToggleRow, onToggleStreamView: onToggleStreamView, streamViewToggled: streamViewToggledRows.has(row.id), onOpenSubagent: onOpenSubagent, onOpenJobs: onOpenJobs, onOpenFile: onOpenFile, setRowRef: setRowRef }, row.id));
897
912
  }), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
898
913
  }
899
914
  /** Load-earlier / show-previous divider row with a mouse hover tint — the
@@ -902,7 +917,7 @@ function ClickableDivider({ title, onClick }) {
902
917
  const [hovered, setHovered] = useState(false);
903
918
  return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
904
919
  }
905
- function TranscriptRow({ rowId, kind, text, textFull, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, smoothStreaming, fresh, thinkingFold, toolBackground, foldTerminalCommand, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, onToggleRow, onToggleStreamView, streamViewToggled, onOpenSubagent, onOpenFile, setRowRef, }) {
920
+ function TranscriptRow({ rowId, kind, text, textFull, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, smoothStreaming, fresh, revealVersion, thinkingFold, toolBackground, foldTerminalCommand, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, job, onToggleRow, onToggleStreamView, streamViewToggled, onOpenSubagent, onOpenJobs, onOpenFile, setRowRef, }) {
906
921
  const ref = React.useCallback((el) => {
907
922
  setRowRef(rowId, el);
908
923
  }, [setRowRef, rowId]);
@@ -988,6 +1003,10 @@ function TranscriptRow({ rowId, kind, text, textFull, executionTarget, streaming
988
1003
  if (!subagent)
989
1004
  return null;
990
1005
  return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick: openSubagent }) }));
1006
+ case 'job':
1007
+ if (!job)
1008
+ return null;
1009
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(JobCard, { job: job, addMargin: addMargin, onClick: onOpenJobs }) }));
991
1010
  }
992
1011
  }
993
1012
  /** Folded compact-summary preview: whitespace flattened, capped with an
@@ -57,6 +57,8 @@ type Props = {
57
57
  /** Live-arrived row (channel `fresh`): gates reveal participation —
58
58
  * replayed cards must paint complete. */
59
59
  fresh?: boolean;
60
+ /** Reveal version supplied by MessageList to avoid one store subscriber per card. */
61
+ revealVersion?: number;
60
62
  };
61
63
  export declare function toolNameColor(raw: string): keyof Theme;
62
64
  /**
@@ -65,6 +67,6 @@ export declare function toolNameColor(raw: string): keyof Theme;
65
67
  * output, read content — instead of the raw result dump (mirroring Claude Code's `AssistantToolUseMessage.tsx` + the dsh-tools presentation views the
66
68
  * channel captures per call).
67
69
  */
68
- export declare function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected, isExpanded, onClick, footnote, diffLayout, toolBackground, onOpenFile, foldTerminalCommand, smoothReveal, fresh, }: Props): React.ReactNode;
70
+ export declare function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected, isExpanded, onClick, footnote, diffLayout, toolBackground, onOpenFile, foldTerminalCommand, smoothReveal, fresh, revealVersion, }: Props): React.ReactNode;
69
71
  export {};
70
72
  //# sourceMappingURL=AssistantToolUseMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantToolUseMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantToolUseMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAA8C,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAMvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAGjE,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACzC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;8CAC0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAsFD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,CAKtD;AA+OD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,UAAkB,EAClB,UAAkB,EAClB,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,cAAuB,EACvB,UAAU,EACV,mBAA2B,EAC3B,YAAoB,EACpB,KAAa,GACd,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAiOzB"}
1
+ {"version":3,"file":"AssistantToolUseMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantToolUseMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAA8C,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAMvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACzC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;8CAC0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAsFD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,CAKtD;AA+OD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,UAAkB,EAClB,UAAkB,EAClB,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,cAAuB,EACvB,UAAU,EACV,mBAA2B,EAC3B,YAAoB,EACpB,KAAa,EACb,aAAa,GACd,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAoOzB"}
@@ -10,6 +10,8 @@ import { SyntaxText } from '../SyntaxText.js';
10
10
  import { useTooltip } from '../Tooltip.js';
11
11
  import { formatDuration } from '../../cc/format.js';
12
12
  import { getRevealVersion, revealLinesOf, snapReveal, subscribeReveal } from '../smoothReveal.js';
13
+ const NOOP_REVEAL_SUBSCRIBE = (_listener) => () => { };
14
+ const getNoRevealVersion = () => 0;
13
15
  /** Tool display names: DSH emits lowercase tool ids (`bash`); Claude Code
14
16
  * shows capitalized names (`Bash`). Map the common ones, fall back to the
15
17
  * id with its first letter uppercased. */
@@ -270,11 +272,11 @@ function HeaderTitle({ name, title, isTerminal, folded, displayArgs, argsLanguag
270
272
  * output, read content — instead of the raw result dump (mirroring Claude Code's `AssistantToolUseMessage.tsx` + the dsh-tools presentation views the
271
273
  * channel captures per call).
272
274
  */
273
- export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected = false, isExpanded = false, onClick, footnote, diffLayout = 'auto', toolBackground = 'none', onOpenFile, foldTerminalCommand = false, smoothReveal = false, fresh = false, }) {
274
- // Reveal-frame wakeups: the shared scheduler bumps its version, which
275
- // re-renders this card even while MemoRow props hold still (the reveal
276
- // lives outside React — see smoothReveal.ts).
277
- React.useSyncExternalStore(subscribeReveal, getRevealVersion);
275
+ export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected = false, isExpanded = false, onClick, footnote, diffLayout = 'auto', toolBackground = 'none', onOpenFile, foldTerminalCommand = false, smoothReveal = false, fresh = false, revealVersion, }) {
276
+ // MessageList owns the single production subscription and passes a version
277
+ // prop only to active reveal rows. Standalone consumers keep the fallback
278
+ // subscription so the component contract remains self-contained.
279
+ React.useSyncExternalStore(revealVersion === undefined ? subscribeReveal : NOOP_REVEAL_SUBSCRIBE, revealVersion === undefined ? getRevealVersion : getNoRevealVersion);
278
280
  const isRunning = tool.status === 'running';
279
281
  const isError = tool.status === 'error';
280
282
  const displayArgs = verbose ? tool.argsFull ?? tool.argsText : tool.argsText;
@@ -14,6 +14,7 @@ import { type SessionModeSpec } from '../sessionModes.js';
14
14
  import { type ScrollGutterMode, type StatusBarConfig, type ToolBackground } from '../tuiDisplayPrefs.js';
15
15
  import { type SubagentState } from './subagents.js';
16
16
  export type { SubagentState } from './subagents.js';
17
+ import { type BackgroundJobState, type BackgroundJobStatus } from './jobs.js';
17
18
  import type { SpinnerMode } from '../components/Spinner/spinnerMode.js';
18
19
  import { type ActivityState } from 'dsh-working-activity/status';
19
20
  import { type TuiWorkspaceCommand, type TuiWorkspaceCommandResult, type TuiWorkspaceTarget } from './workspaces.js';
@@ -138,6 +139,14 @@ export interface ToolViewPresenter {
138
139
  export interface SubagentControl {
139
140
  interrupt(agentId: string): boolean;
140
141
  }
142
+ /**
143
+ * Background-job row control (`/jobs` panel): cancellation with the same
144
+ * authority the owning agent itself would use (`job_kill`). Returns false
145
+ * when the jobs service is absent or the job is unknown/foreign.
146
+ */
147
+ export interface JobControl {
148
+ kill(id: string): boolean;
149
+ }
141
150
  export interface SubagentRow {
142
151
  agentId: string;
143
152
  runId?: string;
@@ -156,6 +165,18 @@ export interface SubagentRow {
156
165
  stopReason?: string;
157
166
  error?: string;
158
167
  }
168
+ /** One background job as a live transcript card (see `kind: 'job'`). */
169
+ export interface JobRow {
170
+ id: string;
171
+ kind: string;
172
+ label: string;
173
+ status: BackgroundJobStatus;
174
+ detail?: string;
175
+ startedAt: number;
176
+ finishedAt?: number;
177
+ /** Mirrored `job_output` tail feeding the card's three-line waterfall. */
178
+ outputLines: readonly string[];
179
+ }
159
180
  /**
160
181
  * One rendered transcript row. The DSH session log is the source of truth:
161
182
  * rows are derived from `session/event` records (and the initial
@@ -163,7 +184,7 @@ export interface SubagentRow {
163
184
  */
164
185
  export interface ChatRow {
165
186
  id: number;
166
- kind: 'user' | 'assistant' | 'tool' | 'notice' | 'reasoning' | 'interrupt' | 'local' | 'local-output' | 'compact' | 'subagent';
187
+ kind: 'user' | 'assistant' | 'tool' | 'notice' | 'reasoning' | 'interrupt' | 'local' | 'local-output' | 'compact' | 'subagent' | 'job';
167
188
  /** Extra label for non-human user rows (e.g. `steering`). */
168
189
  label?: string;
169
190
  /** Actual execution location for `!command` rows. */
@@ -175,6 +196,8 @@ export interface ChatRow {
175
196
  tool?: ToolRow;
176
197
  /** Present on `subagent` rows; the subagent state snapshot. */
177
198
  subagent?: SubagentRow;
199
+ /** Present on `job` rows; the background-job state snapshot. */
200
+ job?: JobRow;
178
201
  /** Event wall-clock time (transcript-mode metadata, assistant rows). */
179
202
  time?: number;
180
203
  /** Present on `reasoning` rows once settled: thinking wall-clock duration. */
@@ -542,6 +565,15 @@ export interface Channel {
542
565
  readonly subagents: readonly SubagentState[];
543
566
  /** Native control operations; unavailable providers safely return false. */
544
567
  readonly subagentControl: SubagentControl;
568
+ /**
569
+ * Background jobs of the current session (`run_in_background` tool work),
570
+ * live-tracked from the harness job registry. Empty when the composition
571
+ * has no jobs service. Drives the `/jobs` panel, transcript job cards and
572
+ * the status-line chip.
573
+ */
574
+ readonly backgroundJobs: readonly BackgroundJobState[];
575
+ /** Cancellation of a background job with the owning agent's authority. */
576
+ readonly jobControl: JobControl;
545
577
  subscribe: (listener: () => void) => () => void;
546
578
  /** Validate and persist a pasted image, returning its prompt placeholder. */
547
579
  stageImage(input: StagedImageInput): Promise<string>;
@@ -979,6 +1011,9 @@ export interface ChannelState {
979
1011
  /** Active subagents roster (see the public Channel type). */
980
1012
  subagents: readonly SubagentState[];
981
1013
  subagentControl: SubagentControl;
1014
+ /** Background jobs of the current session (see the public Channel type). */
1015
+ backgroundJobs: readonly BackgroundJobState[];
1016
+ jobControl: JobControl;
982
1017
  subscribe: (listener: () => void) => () => void;
983
1018
  stageImage(input: StagedImageInput): Promise<string>;
984
1019
  /** @internal event bump (the public `notify(text)` posts a notification). */