@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.
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +1 -0
- package/lib/types/components/Chat/JobCard.d.ts +23 -0
- package/lib/types/components/Chat/JobCard.d.ts.map +1 -0
- package/lib/types/components/Chat/JobCard.js +86 -0
- package/lib/types/components/JobsPanel.d.ts +17 -0
- package/lib/types/components/JobsPanel.d.ts.map +1 -0
- package/lib/types/components/JobsPanel.js +111 -0
- package/lib/types/components/MessageList.d.ts +3 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +35 -16
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +7 -5
- package/lib/types/dsh-adapter/channel.d.ts +36 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +187 -0
- package/lib/types/dsh-adapter/jobs.d.ts +145 -0
- package/lib/types/dsh-adapter/jobs.d.ts.map +1 -0
- package/lib/types/dsh-adapter/jobs.js +202 -0
- package/lib/types/i18n.d.ts +95 -0
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +25 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +12 -5
- package/lib/types/ink/selection.d.ts +31 -1
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +48 -6
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +19 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +27 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +6 -0
- 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,
|
|
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"}
|
package/lib/types/commands.js
CHANGED
|
@@ -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":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAA;AAEvC,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAE3E,OAAO,KAAK,EAAE,OAAO,
|
|
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
|
|
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
|
|
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 &&
|
|
832
|
-
// Layout corrections can cascade for many rows. Yield
|
|
833
|
-
// so React does not count the valid convergence as nested
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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 &&
|
|
849
|
-
|
|
850
|
-
|
|
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;
|
|
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
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
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). */
|