@deepseek-harness-tui/dsh-tui 0.10.0-beta.2 → 0.10.0-beta.4
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/README.md +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +5 -1
- 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 +88 -44
- package/lib/types/components/PromptInput.d.ts +19 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +34 -11
- package/lib/types/components/approvals/ApprovalPanel.d.ts +4 -1
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +2 -2
- 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 +10 -6
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +4 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +6 -1
- package/lib/types/components/smoothReveal.d.ts +0 -40
- package/lib/types/components/smoothReveal.d.ts.map +1 -1
- package/lib/types/components/smoothReveal.js +28 -2
- package/lib/types/dsh-adapter/agent-view.d.ts +66 -0
- package/lib/types/dsh-adapter/agent-view.d.ts.map +1 -0
- package/lib/types/dsh-adapter/agent-view.js +153 -0
- package/lib/types/dsh-adapter/approvals.d.ts +24 -1
- package/lib/types/dsh-adapter/approvals.d.ts.map +1 -1
- package/lib/types/dsh-adapter/approvals.js +37 -1
- package/lib/types/dsh-adapter/channel.d.ts +164 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +1175 -28
- package/lib/types/dsh-adapter/inject-channel.d.ts +108 -0
- package/lib/types/dsh-adapter/inject-channel.d.ts.map +1 -0
- package/lib/types/dsh-adapter/inject-channel.js +247 -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 +26 -6
- package/lib/types/dsh-adapter/providerWizard.d.ts +86 -6
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +684 -186
- package/lib/types/history.d.ts +3 -1
- package/lib/types/history.d.ts.map +1 -1
- package/lib/types/history.js +80 -44
- package/lib/types/hooks/useExternalVersion.d.ts +32 -0
- package/lib/types/hooks/useExternalVersion.d.ts.map +1 -0
- package/lib/types/hooks/useExternalVersion.js +53 -0
- package/lib/types/hooks/useRevealVersion.d.ts +21 -0
- package/lib/types/hooks/useRevealVersion.d.ts.map +1 -0
- package/lib/types/hooks/useRevealVersion.js +33 -0
- package/lib/types/i18n.d.ts +505 -1
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +142 -1
- package/lib/types/ink/components/ClockContext.d.ts +6 -0
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +24 -3
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +12 -2
- package/lib/types/ink/flush-tick.d.ts +26 -0
- package/lib/types/ink/flush-tick.d.ts.map +1 -0
- package/lib/types/ink/flush-tick.js +31 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +78 -12
- package/lib/types/ink/output.js +5 -2
- package/lib/types/ink/render-node-to-output.d.ts +46 -0
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +62 -1
- package/lib/types/ink/selection.d.ts +96 -3
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +220 -9
- package/lib/types/ink/update-overflow-guard.d.ts +63 -0
- package/lib/types/ink/update-overflow-guard.d.ts.map +1 -0
- package/lib/types/ink/update-overflow-guard.js +165 -0
- package/lib/types/screens/AgentView.d.ts +23 -0
- package/lib/types/screens/AgentView.d.ts.map +1 -0
- package/lib/types/screens/AgentView.js +546 -0
- package/lib/types/screens/Chat.d.ts +10 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +238 -19
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +31 -1
- package/lib/types/sessionHistory.d.ts +18 -0
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessionHistory.js +65 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +6 -0
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
- **终端交互**:低资源占用,长会话稳定可靠;多种主题切换,样式美观,实时显示工作状态、TPS、缓存命中率等
|
|
56
56
|
推理等级、输入/输出 token 与 Git/会话信息;终端卡多行命令可经 `/settings` 折叠为首行 + 计数提示(Ctrl+O 或点击卡片展开);全屏模式下悬停在截断的工具卡标题、用户消息或会话标题上约 600ms,浮层显示完整内容。
|
|
57
|
-
- **功能全面**:`/resume` 按工作目录分类浏览、搜索与预览历史会话(左键恢复、右键弹出操作菜单;可固定常用会话——「已固定」分组置顶显示,行内 ★ 或 `Ctrl+P` 切换,持久化到 `~/.dsh-tui`),另有 `/new`、`/compact`、`/export`、`/btw`,模型热切换,原生subagent,会话fork,自动更新,输入框 `/vim` vim 编辑模式、鼠标选区编辑(拖选高亮、Shift+click 扩展、双击选词、Ctrl+C 复制选区)与全屏草稿编辑(`Ctrl+Shift+E` 或输入行 `⛶` 按钮:行号 + 当前行高亮、Enter 换行、Ctrl+Enter 发送、滚轮滚动、点击/拖选,长草稿独占整屏;`/settings` 可关);可在vs code中[以vscode插件形式启动](docs/vscode.md),已上架 VS Code Marketplace。
|
|
57
|
+
- **功能全面**:`/resume` 按工作目录分类浏览、搜索与预览历史会话(左键恢复、右键弹出操作菜单;可固定常用会话——「已固定」分组置顶显示,行内 ★ 或 `Ctrl+P` 切换,持久化到 `~/.dsh-tui`),另有 `/agentview` 会话总览(CC 同款 agent view:空输入 `←` 一键后台化,后台会话派发/预览/回复/停止一站式管理)、`/new`、`/compact`、`/export`、`/btw`,模型热切换,原生subagent,会话fork,自动更新,输入框 `/vim` vim 编辑模式、鼠标选区编辑(拖选高亮、Shift+click 扩展、双击选词、Ctrl+C 复制选区)与全屏草稿编辑(`Ctrl+Shift+E` 或输入行 `⛶` 按钮:行号 + 当前行高亮、Enter 换行、Ctrl+Enter 发送、滚轮滚动、点击/拖选,长草稿独占整屏;`/settings` 可关);可在vs code中[以vscode插件形式启动](docs/vscode.md),已上架 VS Code Marketplace。
|
|
58
58
|
- **扩展丰富**:原生浏览器交互,compter use等大量附属功能性扩展
|
|
59
59
|
- **技能归 DSH 管理**:`/skills` 展示当前 profile、用户与项目发现的技能;dsh-TUI 不预装通用技能。
|
|
60
60
|
|
|
@@ -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,EA+DxC,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
|
@@ -33,6 +33,9 @@ export const LOCAL_COMMANDS = [
|
|
|
33
33
|
{ name: 'export', description: 'Export the conversation to a markdown file' },
|
|
34
34
|
{ name: 'btw', description: 'Ask a quick side question without interrupting the conversation' },
|
|
35
35
|
{ name: 'trace', description: 'Show the session event trace timeline' },
|
|
36
|
+
{ name: 'agentview', description: 'Open the agent view (all sessions)' },
|
|
37
|
+
{ name: 'bg', description: 'Background this session and open agent view' },
|
|
38
|
+
{ name: 'background', description: 'Background this session and open agent view', tag: 'alias of /bg' },
|
|
36
39
|
// Session / environment
|
|
37
40
|
{ name: 'context', description: 'Show loaded context details' },
|
|
38
41
|
{ name: 'status', description: 'Show session status' },
|
|
@@ -43,6 +46,7 @@ export const LOCAL_COMMANDS = [
|
|
|
43
46
|
{ name: 'doctor', description: 'Run environment checks' },
|
|
44
47
|
{ name: 'init', description: 'Create AGENTS.md in the working directory' },
|
|
45
48
|
{ name: 'agents', description: 'Show subagents of this session' },
|
|
49
|
+
{ name: 'jobs', description: 'Show background jobs of this session' },
|
|
46
50
|
// Model / display
|
|
47
51
|
{ name: 'activity', description: 'Switch the working-activity indicator preset' },
|
|
48
52
|
{ name: 'preset', description: 'Switch the agent preset (including Liangshen mode)' },
|
|
@@ -55,7 +59,7 @@ export const LOCAL_COMMANDS = [
|
|
|
55
59
|
{ name: 'tokens', description: 'Show session token usage' },
|
|
56
60
|
// Account / policy
|
|
57
61
|
{ name: 'balance', description: 'Show DeepSeek account balance' },
|
|
58
|
-
{ name: 'provider', description: 'Add an LLM provider (catalog or custom API endpoint)' },
|
|
62
|
+
{ name: 'provider', description: 'Add, edit or delete an LLM provider (catalog or custom API endpoint)' },
|
|
59
63
|
{ name: 'login', description: 'Show API credential status' },
|
|
60
64
|
{ name: 'logout', description: 'Clear the API credential' },
|
|
61
65
|
{ name: 'add-dir', description: 'Show the filesystem policy scope' },
|
|
@@ -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,8 +8,10 @@ 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';
|
|
14
|
+
import { getTerminalFlushTick } from '../ink/flush-tick.js';
|
|
13
15
|
import { InterruptedByUser } from './InterruptedByUser.js';
|
|
14
16
|
import { LogoV2 } from './LogoV2.js';
|
|
15
17
|
import { StreamingMarkdown } from './StreamingMarkdown.js';
|
|
@@ -18,7 +20,8 @@ import { stripNarration } from '../utils/narration.js';
|
|
|
18
20
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
19
21
|
import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
20
22
|
import { clipPreview } from '../ink/timeline-rail.js';
|
|
21
|
-
import { getRevealVersion, revealLengthOf, revealTextOf
|
|
23
|
+
import { getRevealVersion, revealLengthOf, revealTextOf } from './smoothReveal.js';
|
|
24
|
+
import { useRevealVersion } from '../hooks/useRevealVersion.js';
|
|
22
25
|
/**
|
|
23
26
|
* Transcript rows rendered in the Claude Code visual language: user prompts
|
|
24
27
|
* on a grey bubble with a `❯` pointer, assistant text with a `●` bullet and
|
|
@@ -153,7 +156,7 @@ function signatureParts(row, columns, expanded, expandedRows, streamViewToggledR
|
|
|
153
156
|
}
|
|
154
157
|
return signatureScratch;
|
|
155
158
|
}
|
|
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, }) {
|
|
159
|
+
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
160
|
const hiddenCount = rows.length - MAX_RENDERED_ROWS;
|
|
158
161
|
// The thinking filter runs BEFORE virtualization so window indices line up.
|
|
159
162
|
//
|
|
@@ -259,13 +262,17 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
259
262
|
};
|
|
260
263
|
// --- layout virtualization ---------------------------------------------
|
|
261
264
|
const { columns, rows: termRows } = useTerminalSize();
|
|
262
|
-
// Smooth-reveal wakeups: the scheduler's tick bumps this
|
|
265
|
+
// Smooth-reveal wakeups: the scheduler's tick bumps this hook, re-running
|
|
263
266
|
// MessageList so the revealed `text`/line-count reads below feed fresh
|
|
264
267
|
// values through the same MemoRow-prop pipeline an arriving chunk uses —
|
|
265
268
|
// memo miss → re-render → post-commit height re-measure. Without this
|
|
266
269
|
// subscription a reveal living in child state would change row heights
|
|
267
270
|
// invisibly to the virtualization (stale cached heights → blank bands).
|
|
268
|
-
|
|
271
|
+
// DefaultLane on purpose (useRevealVersion): a useSyncExternalStore wakeup
|
|
272
|
+
// would force a SyncLane render per 33ms tick, and each such commit ending
|
|
273
|
+
// with streaming work still pending feeds React's nested-update counter
|
|
274
|
+
// until error #185 kills the process (beta.3).
|
|
275
|
+
useRevealVersion();
|
|
269
276
|
// Measured row heights, remembered after a row unmounts so virtualization
|
|
270
277
|
// can compute total content height. Bounded: row ids grow monotonically
|
|
271
278
|
// and rows are never removed from the transcript (foldRows keeps the
|
|
@@ -288,19 +295,22 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
288
295
|
const paintedOnceRef = React.useRef(new Set());
|
|
289
296
|
const paintedBaseRef = React.useRef(undefined);
|
|
290
297
|
/** Window-expansion hold: after the window WIDENS (new rows mounted),
|
|
291
|
-
* refuse to tighten
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
+
* refuse to tighten until a frame containing that layout has actually
|
|
299
|
+
* been FLUSHED to the terminal (flush-tick based, issue #574). React
|
|
300
|
+
* commits and terminal writes are decoupled — the throttled deferred
|
|
301
|
+
* leading edge lets a later commit supersede the wide one inside the
|
|
302
|
+
* same task, so a wall-clock hold (the old 120ms timer) expires during
|
|
303
|
+
* long cold-cache layout work (~190ms on the repro) and the measure-tick
|
|
304
|
+
* re-render still drops the rows before a single byte of them was
|
|
305
|
+
* written; never-mounted rows have no scrollback copy and preset history
|
|
306
|
+
* vanishes. Once flushed, tightening is visually free: those rows sit in
|
|
307
|
+
* scrollback and the diff skips them. */
|
|
298
308
|
const lastStartRef = React.useRef(-1);
|
|
299
|
-
const
|
|
309
|
+
const holdFlushTickRef = React.useRef(-1);
|
|
300
310
|
/** True when frame-budgeted history painting still has batches left
|
|
301
311
|
* (main-screen open): the layout effect schedules the next slice. */
|
|
302
312
|
const paintPendingRef = React.useRef(false);
|
|
303
|
-
const
|
|
313
|
+
const paintTimerRef = React.useRef(null);
|
|
304
314
|
/** Persistent history-paint edge: how far batched painting has advanced
|
|
305
315
|
* (index into visibleRows). -1 = not painting / reset (list head change:
|
|
306
316
|
* rewind, new session, loadOlder — must repaint from scratch). */
|
|
@@ -316,9 +326,19 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
316
326
|
paintedBaseRef.current = listHeadId;
|
|
317
327
|
/** Content-space offset of visibleRows[0] (header + dividers), measured. */
|
|
318
328
|
const baseRef = React.useRef(null);
|
|
319
|
-
const
|
|
329
|
+
const measureTimerRef = React.useRef(null);
|
|
320
330
|
const [, setMeasureTick] = React.useState(0);
|
|
321
331
|
const [, setScrollTick] = React.useState(0);
|
|
332
|
+
React.useEffect(() => () => {
|
|
333
|
+
if (measureTimerRef.current !== null) {
|
|
334
|
+
clearTimeout(measureTimerRef.current);
|
|
335
|
+
measureTimerRef.current = null;
|
|
336
|
+
}
|
|
337
|
+
if (paintTimerRef.current !== null) {
|
|
338
|
+
clearTimeout(paintTimerRef.current);
|
|
339
|
+
paintTimerRef.current = null;
|
|
340
|
+
}
|
|
341
|
+
}, []);
|
|
322
342
|
// A width change reflows every row — all measurements are stale.
|
|
323
343
|
const lastColumns = React.useRef(columns);
|
|
324
344
|
if (lastColumns.current !== columns) {
|
|
@@ -555,15 +575,18 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
555
575
|
}
|
|
556
576
|
}
|
|
557
577
|
// Expansion hold — AFTER the extension so it tracks the FINAL window:
|
|
558
|
-
// never tighten
|
|
559
|
-
//
|
|
560
|
-
// re-render that drops the row paints only
|
|
561
|
-
// row's painted-once mark (set at the first
|
|
562
|
-
|
|
578
|
+
// never tighten past a widen until a frame with that layout has been
|
|
579
|
+
// flushed (getTerminalFlushTick advanced since the widen). A mount
|
|
580
|
+
// followed by the measure-tick re-render that drops the row paints only
|
|
581
|
+
// the DROPPED layout, and the row's painted-once mark (set at the first
|
|
582
|
+
// commit) is a lie; holding until the flush makes the mark real.
|
|
583
|
+
if (lastStartRef.current >= 0 &&
|
|
584
|
+
start > lastStartRef.current &&
|
|
585
|
+
getTerminalFlushTick() === holdFlushTickRef.current) {
|
|
563
586
|
start = lastStartRef.current;
|
|
564
587
|
}
|
|
565
588
|
if (lastStartRef.current < 0 || start < lastStartRef.current) {
|
|
566
|
-
|
|
589
|
+
holdFlushTickRef.current = getTerminalFlushTick();
|
|
567
590
|
}
|
|
568
591
|
lastStartRef.current = start;
|
|
569
592
|
}
|
|
@@ -752,18 +775,31 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
752
775
|
upId: upTurnIndex === null ? null : timelineTurns[upTurnIndex].id,
|
|
753
776
|
downId: downTurnIndex === null ? null : timelineTurns[downTurnIndex].id,
|
|
754
777
|
};
|
|
755
|
-
const lastTimelineReportRef = React.useRef(
|
|
778
|
+
const lastTimelineReportRef = React.useRef(null);
|
|
756
779
|
React.useEffect(() => {
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
780
|
+
// Value-level dedup, not geometry-key-level: the memo key pins to
|
|
781
|
+
// heightsVersion, which a GROWING streamed row bumps on every commit,
|
|
782
|
+
// so a key-pinned signature re-reports an identical snapshot every
|
|
783
|
+
// commit of a long stream. Each report is a setState dispatched into
|
|
784
|
+
// Chat from this passive effect; it lands as pending residue at commit
|
|
785
|
+
// end, keeping the commit dirty and feeding React's nested-update
|
|
786
|
+
// counter (50 consecutive dirty commits → error #185, the beta.3
|
|
787
|
+
// crash). The O(turns) compare below is integer/string-reference
|
|
788
|
+
// compares only — cheap next to the joined-string signature the memo
|
|
789
|
+
// key replaced, and it fires exactly when the snapshot content changes.
|
|
790
|
+
const prev = lastTimelineReportRef.current;
|
|
791
|
+
if (prev !== null &&
|
|
792
|
+
prev.activeId === timeline.activeId &&
|
|
793
|
+
prev.upId === timeline.upId &&
|
|
794
|
+
prev.downId === timeline.downId &&
|
|
795
|
+
prev.turns.length === timeline.turns.length &&
|
|
796
|
+
prev.turns.every((t, i) => {
|
|
797
|
+
const n = timeline.turns[i];
|
|
798
|
+
return t.id === n.id && t.top === n.top && t.folded === n.folded && t.preview === n.preview;
|
|
799
|
+
}))
|
|
800
|
+
return;
|
|
801
|
+
lastTimelineReportRef.current = timeline;
|
|
802
|
+
onTimeline?.(timeline);
|
|
767
803
|
});
|
|
768
804
|
// Post-commit: measure mounted rows, derive the content-space base from
|
|
769
805
|
// the first mounted row's Yoga top, and clamp render-time scrollTop to the
|
|
@@ -828,15 +864,15 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
828
864
|
scrollHandle.setClampBounds(min, bottomPad <= 0 ? undefined : Math.max(min, base + mountedBottom - viewport));
|
|
829
865
|
}
|
|
830
866
|
}
|
|
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
|
-
|
|
867
|
+
if (changed && measureTimerRef.current === null) {
|
|
868
|
+
// Layout corrections can cascade for many rows. Yield to the next
|
|
869
|
+
// macrotask so React does not count the valid convergence as nested
|
|
870
|
+
// updates when a streaming/reveal commit is already in flight.
|
|
871
|
+
measureTimerRef.current = setTimeout(() => {
|
|
872
|
+
measureTimerRef.current = null;
|
|
837
873
|
noteFrameCause('measure');
|
|
838
874
|
setMeasureTick(t => t + 1);
|
|
839
|
-
});
|
|
875
|
+
}, 0);
|
|
840
876
|
}
|
|
841
877
|
// History-paint continuation (main-screen open): more never-painted
|
|
842
878
|
// batches remain — schedule the next slice on the macrotask queue so
|
|
@@ -845,10 +881,9 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
845
881
|
// enough: each batch's mount+measure work is bounded (~2 viewports),
|
|
846
882
|
// unlike the previous single-commit full mount that saturated the main
|
|
847
883
|
// thread for seconds.
|
|
848
|
-
if (paintPendingRef.current &&
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
paintQueuedRef.current = false;
|
|
884
|
+
if (paintPendingRef.current && paintTimerRef.current === null) {
|
|
885
|
+
paintTimerRef.current = setTimeout(() => {
|
|
886
|
+
paintTimerRef.current = null;
|
|
852
887
|
if (!paintPendingRef.current)
|
|
853
888
|
return;
|
|
854
889
|
noteFrameCause('measure');
|
|
@@ -873,6 +908,11 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
873
908
|
const addMargin = margins.get(row.id) === true;
|
|
874
909
|
const tool = row.tool;
|
|
875
910
|
const subagent = row.kind === 'subagent' ? row.subagent : undefined;
|
|
911
|
+
const job = row.kind === 'job' ? row.job : undefined;
|
|
912
|
+
const revealVersion = smoothStreaming && row.kind === 'tool' && row.fresh === true &&
|
|
913
|
+
row.tool?.status === 'running' && row.tool.resultView === undefined
|
|
914
|
+
? getRevealVersion()
|
|
915
|
+
: 0;
|
|
876
916
|
// Smooth reveal feeds the SAME flattened text prop a chunk feeds,
|
|
877
917
|
// and keeps the streaming layout alive until the reveal catches up
|
|
878
918
|
// (settling mid-reveal must not snap — a one-shot non-streaming
|
|
@@ -893,7 +933,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
893
933
|
else if (row.kind === 'reasoning' && smoothStreaming) {
|
|
894
934
|
displayText = revealTextOf(`r${row.id}`, row.text, { enabled: true, active: displayStreaming });
|
|
895
935
|
}
|
|
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));
|
|
936
|
+
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
937
|
}), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
|
|
898
938
|
}
|
|
899
939
|
/** Load-earlier / show-previous divider row with a mouse hover tint — the
|
|
@@ -902,7 +942,7 @@ function ClickableDivider({ title, onClick }) {
|
|
|
902
942
|
const [hovered, setHovered] = useState(false);
|
|
903
943
|
return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
|
|
904
944
|
}
|
|
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, }) {
|
|
945
|
+
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
946
|
const ref = React.useCallback((el) => {
|
|
907
947
|
setRowRef(rowId, el);
|
|
908
948
|
}, [setRowRef, rowId]);
|
|
@@ -988,6 +1028,10 @@ function TranscriptRow({ rowId, kind, text, textFull, executionTarget, streaming
|
|
|
988
1028
|
if (!subagent)
|
|
989
1029
|
return null;
|
|
990
1030
|
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick: openSubagent }) }));
|
|
1031
|
+
case 'job':
|
|
1032
|
+
if (!job)
|
|
1033
|
+
return null;
|
|
1034
|
+
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(JobCard, { job: job, addMargin: addMargin, onClick: onOpenJobs }) }));
|
|
991
1035
|
}
|
|
992
1036
|
}
|
|
993
1037
|
/** Folded compact-summary preview: whitespace flattened, capped with an
|