@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.
Files changed (93) hide show
  1. package/README.md +1 -1
  2. package/lib/types/commands.d.ts.map +1 -1
  3. package/lib/types/commands.js +5 -1
  4. package/lib/types/components/Chat/JobCard.d.ts +23 -0
  5. package/lib/types/components/Chat/JobCard.d.ts.map +1 -0
  6. package/lib/types/components/Chat/JobCard.js +86 -0
  7. package/lib/types/components/JobsPanel.d.ts +17 -0
  8. package/lib/types/components/JobsPanel.d.ts.map +1 -0
  9. package/lib/types/components/JobsPanel.js +111 -0
  10. package/lib/types/components/MessageList.d.ts +3 -1
  11. package/lib/types/components/MessageList.d.ts.map +1 -1
  12. package/lib/types/components/MessageList.js +88 -44
  13. package/lib/types/components/PromptInput.d.ts +19 -1
  14. package/lib/types/components/PromptInput.d.ts.map +1 -1
  15. package/lib/types/components/PromptInput.js +34 -11
  16. package/lib/types/components/approvals/ApprovalPanel.d.ts +4 -1
  17. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  18. package/lib/types/components/approvals/ApprovalPanel.js +2 -2
  19. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
  20. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  21. package/lib/types/components/messages/AssistantToolUseMessage.js +10 -6
  22. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +4 -0
  23. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  24. package/lib/types/components/questions/AskUserQuestionPanel.js +6 -1
  25. package/lib/types/components/smoothReveal.d.ts +0 -40
  26. package/lib/types/components/smoothReveal.d.ts.map +1 -1
  27. package/lib/types/components/smoothReveal.js +28 -2
  28. package/lib/types/dsh-adapter/agent-view.d.ts +66 -0
  29. package/lib/types/dsh-adapter/agent-view.d.ts.map +1 -0
  30. package/lib/types/dsh-adapter/agent-view.js +153 -0
  31. package/lib/types/dsh-adapter/approvals.d.ts +24 -1
  32. package/lib/types/dsh-adapter/approvals.d.ts.map +1 -1
  33. package/lib/types/dsh-adapter/approvals.js +37 -1
  34. package/lib/types/dsh-adapter/channel.d.ts +164 -1
  35. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  36. package/lib/types/dsh-adapter/channel.js +1175 -28
  37. package/lib/types/dsh-adapter/inject-channel.d.ts +108 -0
  38. package/lib/types/dsh-adapter/inject-channel.d.ts.map +1 -0
  39. package/lib/types/dsh-adapter/inject-channel.js +247 -0
  40. package/lib/types/dsh-adapter/jobs.d.ts +145 -0
  41. package/lib/types/dsh-adapter/jobs.d.ts.map +1 -0
  42. package/lib/types/dsh-adapter/jobs.js +202 -0
  43. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  44. package/lib/types/dsh-adapter/plugin.js +26 -6
  45. package/lib/types/dsh-adapter/providerWizard.d.ts +86 -6
  46. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  47. package/lib/types/dsh-adapter/providerWizard.js +684 -186
  48. package/lib/types/history.d.ts +3 -1
  49. package/lib/types/history.d.ts.map +1 -1
  50. package/lib/types/history.js +80 -44
  51. package/lib/types/hooks/useExternalVersion.d.ts +32 -0
  52. package/lib/types/hooks/useExternalVersion.d.ts.map +1 -0
  53. package/lib/types/hooks/useExternalVersion.js +53 -0
  54. package/lib/types/hooks/useRevealVersion.d.ts +21 -0
  55. package/lib/types/hooks/useRevealVersion.d.ts.map +1 -0
  56. package/lib/types/hooks/useRevealVersion.js +33 -0
  57. package/lib/types/i18n.d.ts +505 -1
  58. package/lib/types/i18n.d.ts.map +1 -1
  59. package/lib/types/i18n.js +142 -1
  60. package/lib/types/ink/components/ClockContext.d.ts +6 -0
  61. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  62. package/lib/types/ink/components/ClockContext.js +24 -3
  63. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  64. package/lib/types/ink/components/ScrollBox.js +12 -2
  65. package/lib/types/ink/flush-tick.d.ts +26 -0
  66. package/lib/types/ink/flush-tick.d.ts.map +1 -0
  67. package/lib/types/ink/flush-tick.js +31 -0
  68. package/lib/types/ink/ink.d.ts.map +1 -1
  69. package/lib/types/ink/ink.js +78 -12
  70. package/lib/types/ink/output.js +5 -2
  71. package/lib/types/ink/render-node-to-output.d.ts +46 -0
  72. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  73. package/lib/types/ink/render-node-to-output.js +62 -1
  74. package/lib/types/ink/selection.d.ts +96 -3
  75. package/lib/types/ink/selection.d.ts.map +1 -1
  76. package/lib/types/ink/selection.js +220 -9
  77. package/lib/types/ink/update-overflow-guard.d.ts +63 -0
  78. package/lib/types/ink/update-overflow-guard.d.ts.map +1 -0
  79. package/lib/types/ink/update-overflow-guard.js +165 -0
  80. package/lib/types/screens/AgentView.d.ts +23 -0
  81. package/lib/types/screens/AgentView.d.ts.map +1 -0
  82. package/lib/types/screens/AgentView.js +546 -0
  83. package/lib/types/screens/Chat.d.ts +10 -1
  84. package/lib/types/screens/Chat.d.ts.map +1 -1
  85. package/lib/types/screens/Chat.js +238 -19
  86. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  87. package/lib/types/screens/StatusLine.js +31 -1
  88. package/lib/types/sessionHistory.d.ts +18 -0
  89. package/lib/types/sessionHistory.d.ts.map +1 -1
  90. package/lib/types/sessionHistory.js +65 -0
  91. package/lib/types/tips.d.ts.map +1 -1
  92. package/lib/types/tips.js +6 -0
  93. 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,EA2DxC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAElD,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAEpD,CAAA;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAIhG;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIhD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,OAAO,CAKT;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,SAAS,YAAY,EAAmB,GAC7C,YAAY,EAAE,CAKhB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,SAAS,YAAY,EAAmB,EAC/C,QAAQ,GAAE,eAA0B,GACnC,iBAAiB,EAAE,CAuCrB"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAElF,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,2EAA2E;AAC3E,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,KAAK,SAAS,qBAAqB,EAAE,CAAA;AAEpG;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,YAAY,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"}
@@ -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":"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"}
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, subscribeReveal } from './smoothReveal.js';
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 store, re-running
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
- React.useSyncExternalStore(subscribeReveal, getRevealVersion);
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 for a short hold so the mounted rows actually reach
292
- * the terminal. React commits within one ink frame coalesce — a render
293
- * that mounts rows followed by the measure-tick re-render that drops
294
- * them paints only the DROPPED layout, and never-mounted rows have no
295
- * scrollback copy (preset history at boot vanished — CI
296
- * repro-inline-scrollback). After the hold, tightening is visually
297
- * free: those rows sit in scrollback and the diff skips them. */
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 holdUntilRef = React.useRef(0);
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 paintQueuedRef = React.useRef(false);
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 measureQueuedRef = React.useRef(false);
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 within the hold window after a widen. React commits
559
- // inside one ink frame coalesce; a mount followed by the measure-tick
560
- // re-render that drops the row paints only the DROPPED layout, and the
561
- // row's painted-once mark (set at the first commit) is a lie.
562
- if (lastStartRef.current >= 0 && start > lastStartRef.current && performance.now() < holdUntilRef.current) {
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
- holdUntilRef.current = performance.now() + 120;
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
- // O(1) signature: the memo key pins the turns' geometry identity
758
- // (heights/window/base/rows-length) and the three ids pin the
759
- // viewport-derived targets. Any top change bumps the geometry key, so
760
- // the report still fires exactly when the snapshot content changes —
761
- // without rebuilding an O(turns) joined string per commit.
762
- const sig = `${timelineMemoRef.current?.key ?? ''}#a${timeline.activeId}#u${timeline.upId}#d${timeline.downId}`;
763
- if (sig !== lastTimelineReportRef.current) {
764
- lastTimelineReportRef.current = sig;
765
- onTimeline?.(timeline);
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 && !measureQueuedRef.current) {
832
- // Layout corrections can cascade for many rows. Yield between commits
833
- // so React does not count the valid convergence as nested updates.
834
- measureQueuedRef.current = true;
835
- queueMicrotask(() => {
836
- measureQueuedRef.current = false;
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 && !paintQueuedRef.current) {
849
- paintQueuedRef.current = true;
850
- setTimeout(() => {
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