@deepseek-harness-tui/dsh-tui 0.10.0-beta.1 → 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 +7 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +102 -22
- package/lib/types/components/SplitDiffView.d.ts +11 -1
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +9 -2
- package/lib/types/components/design-system/Divider.d.ts +10 -5
- package/lib/types/components/design-system/Divider.d.ts.map +1 -1
- package/lib/types/components/design-system/Divider.js +51 -7
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +6 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +6 -2
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +15 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +24 -2
- package/lib/types/components/smoothReveal.d.ts +97 -0
- package/lib/types/components/smoothReveal.d.ts.map +1 -0
- package/lib/types/components/smoothReveal.js +279 -0
- package/lib/types/dsh-adapter/channel.d.ts +53 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +203 -1
- package/lib/types/dsh-adapter/index.d.ts +5 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +1 -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/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +16 -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/parse-keypress.d.ts +18 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +178 -7
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +8 -1
- 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 +3 -2
|
@@ -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, 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>;
|
|
@@ -22,6 +22,10 @@ export declare function MessageList({ rows, expanded, expandedRows, selectedId,
|
|
|
22
22
|
toolBackground?: ToolBackground;
|
|
23
23
|
/** Terminal-card header folding from the live channel settings. */
|
|
24
24
|
foldTerminalCommand?: boolean;
|
|
25
|
+
/** Smooth streaming reveal from the live channel settings (default off at
|
|
26
|
+
* this layer — embedders and verify harnesses keep exact-paint behavior;
|
|
27
|
+
* Chat passes the channel's `dsh-tui.smoothStreaming` value). */
|
|
28
|
+
smoothStreaming?: boolean;
|
|
25
29
|
/** Working-activity preset name from the channel; drives the subagent
|
|
26
30
|
* card's running glyph so both indicators follow one setting. */
|
|
27
31
|
activityFrames?: string;
|
|
@@ -84,6 +88,8 @@ export declare function MessageList({ rows, expanded, expandedRows, selectedId,
|
|
|
84
88
|
failureHint?: string;
|
|
85
89
|
/** 打开子代理详情场景(transcript 内点击子代理卡)。 */
|
|
86
90
|
onOpenSubagent?: (agentId: string) => void;
|
|
91
|
+
/** 打开 /jobs 后台任务面板(transcript 内点击任务卡)。 */
|
|
92
|
+
onOpenJobs?: () => void;
|
|
87
93
|
/** 点击工具卡内的文件路径(打开文件操作菜单)。 */
|
|
88
94
|
onOpenFile?: (path: string) => void;
|
|
89
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';
|
|
@@ -18,6 +19,7 @@ import { stripNarration } from '../utils/narration.js';
|
|
|
18
19
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
19
20
|
import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
20
21
|
import { clipPreview } from '../ink/timeline-rail.js';
|
|
22
|
+
import { getRevealVersion, revealLengthOf, revealTextOf, subscribeReveal } from './smoothReveal.js';
|
|
21
23
|
/**
|
|
22
24
|
* Transcript rows rendered in the Claude Code visual language: user prompts
|
|
23
25
|
* on a grey bubble with a `❯` pointer, assistant text with a `●` bullet and
|
|
@@ -55,6 +57,37 @@ const DEFAULT_HEADER_LINES = 14;
|
|
|
55
57
|
* toggle callback's deps never churn. */
|
|
56
58
|
const NO_STREAM_VIEW_TOGGLED = new Set();
|
|
57
59
|
const NOOP_TOGGLE_STREAM_VIEW = (_rowId) => { };
|
|
60
|
+
// --- smooth-streaming display text -----------------------------------------
|
|
61
|
+
// Render-phase reads of the shared reveal cursors (see smoothReveal.ts for
|
|
62
|
+
// why the cursors live outside React). `active` gates cursor CREATION to
|
|
63
|
+
// live-arrived content (streaming rows, live-settled rows marked `fresh`,
|
|
64
|
+
// replayed history paints complete); once created, a cursor keeps revealing
|
|
65
|
+
// until it catches up — settling mid-reveal must not snap (that is the
|
|
66
|
+
// "non-streaming delivery becomes a smooth flow" contract).
|
|
67
|
+
function assistantRevealText(row, enabled) {
|
|
68
|
+
const stripped = stripNarration(row.text);
|
|
69
|
+
return revealTextOf(`a${row.id}`, stripped, {
|
|
70
|
+
enabled,
|
|
71
|
+
active: row.streaming === true || row.fresh === true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function reasoningRevealText(row, enabled) {
|
|
75
|
+
return revealTextOf(`r${row.id}`, row.text, { enabled, active: row.streaming === true });
|
|
76
|
+
}
|
|
77
|
+
/** Display length only (layout signature; no slice allocation). */
|
|
78
|
+
function revealDisplayLen(row, enabled) {
|
|
79
|
+
if (row.kind === 'assistant') {
|
|
80
|
+
const stripped = stripNarration(row.text);
|
|
81
|
+
return revealLengthOf(`a${row.id}`, stripped, {
|
|
82
|
+
enabled,
|
|
83
|
+
active: row.streaming === true || row.fresh === true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (row.kind === 'reasoning') {
|
|
87
|
+
return revealLengthOf(`r${row.id}`, row.text, { enabled, active: row.streaming === true });
|
|
88
|
+
}
|
|
89
|
+
return row.text.length;
|
|
90
|
+
}
|
|
58
91
|
/**
|
|
59
92
|
* Per-kind layout signature PARTS: the O(1) identity of every input that
|
|
60
93
|
* decides a row's rendered HEIGHT (see sigRef in MessageList). Fields are
|
|
@@ -76,11 +109,13 @@ const NOOP_TOGGLE_STREAM_VIEW = (_rowId) => { };
|
|
|
76
109
|
* (parts.slice()).
|
|
77
110
|
*/
|
|
78
111
|
const signatureScratch = [];
|
|
79
|
-
function signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint) {
|
|
112
|
+
function signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint, displayTextLen) {
|
|
80
113
|
signatureScratch.length = 0;
|
|
81
114
|
// Universal height inputs: width reflows every row; kind switches height
|
|
82
|
-
// semantics wholesale; text length drives wrapping.
|
|
83
|
-
|
|
115
|
+
// semantics wholesale; text length drives wrapping. `displayTextLen` is the
|
|
116
|
+
// REVEALED length while smooth streaming is painting (the height follows
|
|
117
|
+
// what is on screen, not what has arrived).
|
|
118
|
+
signatureScratch.push(columns, row.kind, displayTextLen);
|
|
84
119
|
switch (row.kind) {
|
|
85
120
|
case 'assistant':
|
|
86
121
|
// Streaming vs settled swaps renderers; Ctrl+O/per-row expand adds the
|
|
@@ -119,7 +154,7 @@ function signatureParts(row, columns, expanded, expandedRows, streamViewToggledR
|
|
|
119
154
|
}
|
|
120
155
|
return signatureScratch;
|
|
121
156
|
}
|
|
122
|
-
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, 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, }) {
|
|
123
158
|
const hiddenCount = rows.length - MAX_RENDERED_ROWS;
|
|
124
159
|
// The thinking filter runs BEFORE virtualization so window indices line up.
|
|
125
160
|
//
|
|
@@ -225,6 +260,13 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
225
260
|
};
|
|
226
261
|
// --- layout virtualization ---------------------------------------------
|
|
227
262
|
const { columns, rows: termRows } = useTerminalSize();
|
|
263
|
+
// Smooth-reveal wakeups: the scheduler's tick bumps this store, re-running
|
|
264
|
+
// MessageList so the revealed `text`/line-count reads below feed fresh
|
|
265
|
+
// values through the same MemoRow-prop pipeline an arriving chunk uses —
|
|
266
|
+
// memo miss → re-render → post-commit height re-measure. Without this
|
|
267
|
+
// subscription a reveal living in child state would change row heights
|
|
268
|
+
// invisibly to the virtualization (stale cached heights → blank bands).
|
|
269
|
+
React.useSyncExternalStore(subscribeReveal, getRevealVersion);
|
|
228
270
|
// Measured row heights, remembered after a row unmounts so virtualization
|
|
229
271
|
// can compute total content height. Bounded: row ids grow monotonically
|
|
230
272
|
// and rows are never removed from the transcript (foldRows keeps the
|
|
@@ -259,7 +301,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
259
301
|
/** True when frame-budgeted history painting still has batches left
|
|
260
302
|
* (main-screen open): the layout effect schedules the next slice. */
|
|
261
303
|
const paintPendingRef = React.useRef(false);
|
|
262
|
-
const
|
|
304
|
+
const paintTimerRef = React.useRef(null);
|
|
263
305
|
/** Persistent history-paint edge: how far batched painting has advanced
|
|
264
306
|
* (index into visibleRows). -1 = not painting / reset (list head change:
|
|
265
307
|
* rewind, new session, loadOlder — must repaint from scratch). */
|
|
@@ -275,9 +317,19 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
275
317
|
paintedBaseRef.current = listHeadId;
|
|
276
318
|
/** Content-space offset of visibleRows[0] (header + dividers), measured. */
|
|
277
319
|
const baseRef = React.useRef(null);
|
|
278
|
-
const
|
|
320
|
+
const measureTimerRef = React.useRef(null);
|
|
279
321
|
const [, setMeasureTick] = React.useState(0);
|
|
280
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
|
+
}, []);
|
|
281
333
|
// A width change reflows every row — all measurements are stale.
|
|
282
334
|
const lastColumns = React.useRef(columns);
|
|
283
335
|
if (lastColumns.current !== columns) {
|
|
@@ -318,7 +370,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
318
370
|
// reasoning height at once, remounting the widened window over rows
|
|
319
371
|
// whose rendering never changed (Yoga spike + measure churn for
|
|
320
372
|
// nothing). Base parts cover the universal height inputs.
|
|
321
|
-
const parts = signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint);
|
|
373
|
+
const parts = signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint, revealDisplayLen(row, smoothStreaming));
|
|
322
374
|
const cachedParts = sigs.get(row.id);
|
|
323
375
|
let same = false;
|
|
324
376
|
if (cachedParts !== undefined && cachedParts.length === parts.length) {
|
|
@@ -787,15 +839,15 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
787
839
|
scrollHandle.setClampBounds(min, bottomPad <= 0 ? undefined : Math.max(min, base + mountedBottom - viewport));
|
|
788
840
|
}
|
|
789
841
|
}
|
|
790
|
-
if (changed &&
|
|
791
|
-
// Layout corrections can cascade for many rows. Yield
|
|
792
|
-
// so React does not count the valid convergence as nested
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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;
|
|
796
848
|
noteFrameCause('measure');
|
|
797
849
|
setMeasureTick(t => t + 1);
|
|
798
|
-
});
|
|
850
|
+
}, 0);
|
|
799
851
|
}
|
|
800
852
|
// History-paint continuation (main-screen open): more never-painted
|
|
801
853
|
// batches remain — schedule the next slice on the macrotask queue so
|
|
@@ -804,10 +856,9 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
804
856
|
// enough: each batch's mount+measure work is bounded (~2 viewports),
|
|
805
857
|
// unlike the previous single-commit full mount that saturated the main
|
|
806
858
|
// thread for seconds.
|
|
807
|
-
if (paintPendingRef.current &&
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
paintQueuedRef.current = false;
|
|
859
|
+
if (paintPendingRef.current && paintTimerRef.current === null) {
|
|
860
|
+
paintTimerRef.current = setTimeout(() => {
|
|
861
|
+
paintTimerRef.current = null;
|
|
811
862
|
if (!paintPendingRef.current)
|
|
812
863
|
return;
|
|
813
864
|
noteFrameCause('measure');
|
|
@@ -832,7 +883,32 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
832
883
|
const addMargin = margins.get(row.id) === true;
|
|
833
884
|
const tool = row.tool;
|
|
834
885
|
const subagent = row.kind === 'subagent' ? row.subagent : undefined;
|
|
835
|
-
|
|
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;
|
|
891
|
+
// Smooth reveal feeds the SAME flattened text prop a chunk feeds,
|
|
892
|
+
// and keeps the streaming layout alive until the reveal catches up
|
|
893
|
+
// (settling mid-reveal must not snap — a one-shot non-streaming
|
|
894
|
+
// delivery still paints as a flow).
|
|
895
|
+
let displayText = row.text;
|
|
896
|
+
let displayStreaming = row.streaming === true;
|
|
897
|
+
if (row.kind === 'assistant' && smoothStreaming) {
|
|
898
|
+
const stripped = stripNarration(row.text);
|
|
899
|
+
displayText = revealTextOf(`a${row.id}`, stripped, {
|
|
900
|
+
enabled: true,
|
|
901
|
+
active: displayStreaming || row.fresh === true,
|
|
902
|
+
});
|
|
903
|
+
displayStreaming = displayStreaming || displayText.length !== stripped.length;
|
|
904
|
+
}
|
|
905
|
+
else if (row.kind === 'assistant') {
|
|
906
|
+
displayText = stripNarration(row.text);
|
|
907
|
+
}
|
|
908
|
+
else if (row.kind === 'reasoning' && smoothStreaming) {
|
|
909
|
+
displayText = revealTextOf(`r${row.id}`, row.text, { enabled: true, active: displayStreaming });
|
|
910
|
+
}
|
|
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));
|
|
836
912
|
}), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
|
|
837
913
|
}
|
|
838
914
|
/** Load-earlier / show-previous divider row with a mouse hover tint — the
|
|
@@ -841,7 +917,7 @@ function ClickableDivider({ title, onClick }) {
|
|
|
841
917
|
const [hovered, setHovered] = useState(false);
|
|
842
918
|
return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
|
|
843
919
|
}
|
|
844
|
-
function TranscriptRow({ rowId, kind, text, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, 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, }) {
|
|
845
921
|
const ref = React.useCallback((el) => {
|
|
846
922
|
setRowRef(rowId, el);
|
|
847
923
|
}, [setRowRef, rowId]);
|
|
@@ -878,7 +954,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
|
|
|
878
954
|
// or per-row transcript expansion always wins and shows the full text.
|
|
879
955
|
const streamPreview = streaming && !expanded && !isExpanded &&
|
|
880
956
|
(streamViewToggled ? thinkingFold === 'full' : thinkingFold === 'preview');
|
|
881
|
-
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, addMargin: addMargin, streaming: streaming, preview: streamPreview,
|
|
957
|
+
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, textFull: textFull, addMargin: addMargin, streaming: streaming, preview: streamPreview,
|
|
882
958
|
// Settled rows keep the fold-on-settle default and expand via
|
|
883
959
|
// expandedRows/Ctrl+O; a live row is always preview or full.
|
|
884
960
|
verbose: isExpanded || expanded || (streaming && !streamPreview), durationMs: durationMs, isSelected: isSelected, onClick: streaming ? streamViewOnClick : foldOnClick }) }));
|
|
@@ -907,7 +983,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
|
|
|
907
983
|
startedAt: toolStartedAt,
|
|
908
984
|
durationMs: toolDurationMs,
|
|
909
985
|
};
|
|
910
|
-
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground, foldTerminalCommand: foldTerminalCommand, onClick: foldOnClick, onOpenFile: onOpenFile }) }));
|
|
986
|
+
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground, smoothReveal: smoothStreaming, fresh: fresh, foldTerminalCommand: foldTerminalCommand, onClick: foldOnClick, onOpenFile: onOpenFile }) }));
|
|
911
987
|
}
|
|
912
988
|
case 'notice':
|
|
913
989
|
return (_jsx(Box, { marginTop: 1, ref: ref, children: _jsx(Divider, { title: ` ${text} ` }) }));
|
|
@@ -927,6 +1003,10 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
|
|
|
927
1003
|
if (!subagent)
|
|
928
1004
|
return null;
|
|
929
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 }) }));
|
|
930
1010
|
}
|
|
931
1011
|
}
|
|
932
1012
|
/** Folded compact-summary preview: whitespace flattened, capped with an
|
|
@@ -3,7 +3,7 @@ import type { ToolFileDiff } from '../dsh-adapter/channel.js';
|
|
|
3
3
|
export { chalkFromToken } from '../cc/syntaxTheme.js';
|
|
4
4
|
export { parseAnsiRuns, highlightLines } from '../cc/syntaxRuns.js';
|
|
5
5
|
import type { ToolBackground } from '../tuiDisplayPrefs.js';
|
|
6
|
-
export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground, }: {
|
|
6
|
+
export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground, reveal, }: {
|
|
7
7
|
readonly diffs: readonly ToolFileDiff[];
|
|
8
8
|
/** Content width available to the whole two-pane block (divider included). */
|
|
9
9
|
readonly width: number;
|
|
@@ -11,5 +11,15 @@ export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBack
|
|
|
11
11
|
readonly maxRows: number;
|
|
12
12
|
readonly verbose: boolean;
|
|
13
13
|
readonly toolBackground?: ToolBackground;
|
|
14
|
+
/**
|
|
15
|
+
* Smooth-streaming participation (the card owning this view computes
|
|
16
|
+
* eligibility): when present, the capped row list reveals line-by-line at
|
|
17
|
+
* the shared ~30fps cadence instead of painting as one block. The `+N
|
|
18
|
+
* lines` fold hint stays rendered throughout — it describes the cap, not
|
|
19
|
+
* the reveal.
|
|
20
|
+
*/
|
|
21
|
+
readonly reveal?: {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
};
|
|
14
24
|
}): React.ReactNode;
|
|
15
25
|
//# sourceMappingURL=SplitDiffView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SplitDiffView.d.ts","sourceRoot":"","sources":["../../../src/components/SplitDiffView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAO7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"SplitDiffView.d.ts","sourceRoot":"","sources":["../../../src/components/SplitDiffView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAO7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAwR3D,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,cAAuB,EACvB,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C,GAAG,KAAK,CAAC,SAAS,CAwFlB"}
|