@deepseek-harness-tui/dsh-tui 0.10.0-beta.1 → 0.10.0-beta.2

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 (34) hide show
  1. package/lib/types/components/MessageList.d.ts +5 -1
  2. package/lib/types/components/MessageList.d.ts.map +1 -1
  3. package/lib/types/components/MessageList.js +70 -9
  4. package/lib/types/components/SplitDiffView.d.ts +11 -1
  5. package/lib/types/components/SplitDiffView.d.ts.map +1 -1
  6. package/lib/types/components/SplitDiffView.js +9 -2
  7. package/lib/types/components/design-system/Divider.d.ts +10 -5
  8. package/lib/types/components/design-system/Divider.d.ts.map +1 -1
  9. package/lib/types/components/design-system/Divider.js +51 -7
  10. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +6 -1
  11. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  12. package/lib/types/components/messages/AssistantThinkingMessage.js +6 -2
  13. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +13 -1
  14. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  15. package/lib/types/components/messages/AssistantToolUseMessage.js +22 -2
  16. package/lib/types/components/smoothReveal.d.ts +97 -0
  17. package/lib/types/components/smoothReveal.d.ts.map +1 -0
  18. package/lib/types/components/smoothReveal.js +279 -0
  19. package/lib/types/dsh-adapter/channel.d.ts +17 -0
  20. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  21. package/lib/types/dsh-adapter/channel.js +16 -1
  22. package/lib/types/dsh-adapter/index.d.ts +5 -0
  23. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  24. package/lib/types/dsh-adapter/index.js +1 -0
  25. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  26. package/lib/types/dsh-adapter/plugin.js +16 -0
  27. package/lib/types/ink/parse-keypress.d.ts +18 -0
  28. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  29. package/lib/types/ink/parse-keypress.js +178 -7
  30. package/lib/types/ink/renderer.d.ts.map +1 -1
  31. package/lib/types/ink/renderer.js +8 -1
  32. package/lib/types/screens/Chat.d.ts.map +1 -1
  33. package/lib/types/screens/Chat.js +1 -1
  34. package/package.json +3 -2
@@ -4,7 +4,7 @@ import type { ChatRow } from '../dsh-adapter/channel.js';
4
4
  import type { DOMElement } from '../ink/dom.js';
5
5
  import { type TimelineSnapshot } from '../ink/timeline-rail.js';
6
6
  import type { ToolBackground } from '../tuiDisplayPrefs.js';
7
- export declare function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows, onToggleStreamView, model, diffLayout, thinkingFold, toolBackground, foldTerminalCommand, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible, historyPaintEnabled, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }: {
7
+ export declare function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows, onToggleStreamView, model, diffLayout, thinkingFold, toolBackground, foldTerminalCommand, smoothStreaming, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible, historyPaintEnabled, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }: {
8
8
  rows: readonly ChatRow[];
9
9
  expanded: boolean;
10
10
  expandedRows: ReadonlySet<number>;
@@ -22,6 +22,10 @@ export declare function MessageList({ rows, expanded, expandedRows, selectedId,
22
22
  toolBackground?: ToolBackground;
23
23
  /** Terminal-card header folding from the live channel settings. */
24
24
  foldTerminalCommand?: boolean;
25
+ /** Smooth streaming reveal from the live channel settings (default off at
26
+ * this layer — embedders and verify harnesses keep exact-paint behavior;
27
+ * Chat passes the channel's `dsh-tui.smoothStreaming` value). */
28
+ smoothStreaming?: boolean;
25
29
  /** Working-activity preset name from the channel; drives the subagent
26
30
  * card's running glyph so both indicators follow one setting. */
27
31
  activityFrames?: string;
@@ -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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"}
@@ -18,6 +18,7 @@ import { stripNarration } from '../utils/narration.js';
18
18
  import { stringWidth } from '../ink/stringWidth.js';
19
19
  import { truncateToWidth } from '../ink/truncateToWidth.js';
20
20
  import { clipPreview } from '../ink/timeline-rail.js';
21
+ import { getRevealVersion, revealLengthOf, revealTextOf, subscribeReveal } from './smoothReveal.js';
21
22
  /**
22
23
  * Transcript rows rendered in the Claude Code visual language: user prompts
23
24
  * on a grey bubble with a `❯` pointer, assistant text with a `●` bullet and
@@ -55,6 +56,37 @@ const DEFAULT_HEADER_LINES = 14;
55
56
  * toggle callback's deps never churn. */
56
57
  const NO_STREAM_VIEW_TOGGLED = new Set();
57
58
  const NOOP_TOGGLE_STREAM_VIEW = (_rowId) => { };
59
+ // --- smooth-streaming display text -----------------------------------------
60
+ // Render-phase reads of the shared reveal cursors (see smoothReveal.ts for
61
+ // why the cursors live outside React). `active` gates cursor CREATION to
62
+ // live-arrived content (streaming rows, live-settled rows marked `fresh`,
63
+ // replayed history paints complete); once created, a cursor keeps revealing
64
+ // until it catches up — settling mid-reveal must not snap (that is the
65
+ // "non-streaming delivery becomes a smooth flow" contract).
66
+ function assistantRevealText(row, enabled) {
67
+ const stripped = stripNarration(row.text);
68
+ return revealTextOf(`a${row.id}`, stripped, {
69
+ enabled,
70
+ active: row.streaming === true || row.fresh === true,
71
+ });
72
+ }
73
+ function reasoningRevealText(row, enabled) {
74
+ return revealTextOf(`r${row.id}`, row.text, { enabled, active: row.streaming === true });
75
+ }
76
+ /** Display length only (layout signature; no slice allocation). */
77
+ function revealDisplayLen(row, enabled) {
78
+ if (row.kind === 'assistant') {
79
+ const stripped = stripNarration(row.text);
80
+ return revealLengthOf(`a${row.id}`, stripped, {
81
+ enabled,
82
+ active: row.streaming === true || row.fresh === true,
83
+ });
84
+ }
85
+ if (row.kind === 'reasoning') {
86
+ return revealLengthOf(`r${row.id}`, row.text, { enabled, active: row.streaming === true });
87
+ }
88
+ return row.text.length;
89
+ }
58
90
  /**
59
91
  * Per-kind layout signature PARTS: the O(1) identity of every input that
60
92
  * decides a row's rendered HEIGHT (see sigRef in MessageList). Fields are
@@ -76,11 +108,13 @@ const NOOP_TOGGLE_STREAM_VIEW = (_rowId) => { };
76
108
  * (parts.slice()).
77
109
  */
78
110
  const signatureScratch = [];
79
- function signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint) {
111
+ function signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint, displayTextLen) {
80
112
  signatureScratch.length = 0;
81
113
  // Universal height inputs: width reflows every row; kind switches height
82
- // semantics wholesale; text length drives wrapping.
83
- signatureScratch.push(columns, row.kind, row.text?.length ?? 0);
114
+ // semantics wholesale; text length drives wrapping. `displayTextLen` is the
115
+ // REVEALED length while smooth streaming is painting (the height follows
116
+ // what is on screen, not what has arrived).
117
+ signatureScratch.push(columns, row.kind, displayTextLen);
84
118
  switch (row.kind) {
85
119
  case 'assistant':
86
120
  // Streaming vs settled swaps renderers; Ctrl+O/per-row expand adds the
@@ -119,7 +153,7 @@ function signatureParts(row, columns, expanded, expandedRows, streamViewToggledR
119
153
  }
120
154
  return signatureScratch;
121
155
  }
122
- export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamViewToggledRows = NO_STREAM_VIEW_TOGGLED, onToggleStreamView = NOOP_TOGGLE_STREAM_VIEW, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', foldTerminalCommand = false, activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, historyPaintEnabled = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, onOpenFile, }) {
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, }) {
123
157
  const hiddenCount = rows.length - MAX_RENDERED_ROWS;
124
158
  // The thinking filter runs BEFORE virtualization so window indices line up.
125
159
  //
@@ -225,6 +259,13 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
225
259
  };
226
260
  // --- layout virtualization ---------------------------------------------
227
261
  const { columns, rows: termRows } = useTerminalSize();
262
+ // Smooth-reveal wakeups: the scheduler's tick bumps this store, re-running
263
+ // MessageList so the revealed `text`/line-count reads below feed fresh
264
+ // values through the same MemoRow-prop pipeline an arriving chunk uses —
265
+ // memo miss → re-render → post-commit height re-measure. Without this
266
+ // subscription a reveal living in child state would change row heights
267
+ // invisibly to the virtualization (stale cached heights → blank bands).
268
+ React.useSyncExternalStore(subscribeReveal, getRevealVersion);
228
269
  // Measured row heights, remembered after a row unmounts so virtualization
229
270
  // can compute total content height. Bounded: row ids grow monotonically
230
271
  // and rows are never removed from the transcript (foldRows keeps the
@@ -318,7 +359,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
318
359
  // reasoning height at once, remounting the widened window over rows
319
360
  // whose rendering never changed (Yoga spike + measure churn for
320
361
  // nothing). Base parts cover the universal height inputs.
321
- const parts = signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint);
362
+ const parts = signatureParts(row, columns, expanded, expandedRows, streamViewToggledRows, thinkingVisible, thinkingFold, diffLayout, foldTerminalCommand, model, failureHintRowId, failureHint, revealDisplayLen(row, smoothStreaming));
322
363
  const cachedParts = sigs.get(row.id);
323
364
  let same = false;
324
365
  if (cachedParts !== undefined && cachedParts.length === parts.length) {
@@ -832,7 +873,27 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
832
873
  const addMargin = margins.get(row.id) === true;
833
874
  const tool = row.tool;
834
875
  const subagent = row.kind === 'subagent' ? row.subagent : undefined;
835
- return (_jsx(MemoRow, { rowId: row.id, kind: row.kind, text: row.text, executionTarget: row.executionTarget, streaming: row.streaming === true, 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, 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));
876
+ // Smooth reveal feeds the SAME flattened text prop a chunk feeds,
877
+ // and keeps the streaming layout alive until the reveal catches up
878
+ // (settling mid-reveal must not snap — a one-shot non-streaming
879
+ // delivery still paints as a flow).
880
+ let displayText = row.text;
881
+ let displayStreaming = row.streaming === true;
882
+ if (row.kind === 'assistant' && smoothStreaming) {
883
+ const stripped = stripNarration(row.text);
884
+ displayText = revealTextOf(`a${row.id}`, stripped, {
885
+ enabled: true,
886
+ active: displayStreaming || row.fresh === true,
887
+ });
888
+ displayStreaming = displayStreaming || displayText.length !== stripped.length;
889
+ }
890
+ else if (row.kind === 'assistant') {
891
+ displayText = stripNarration(row.text);
892
+ }
893
+ else if (row.kind === 'reasoning' && smoothStreaming) {
894
+ displayText = revealTextOf(`r${row.id}`, row.text, { enabled: true, active: displayStreaming });
895
+ }
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));
836
897
  }), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
837
898
  }
838
899
  /** Load-earlier / show-previous divider row with a mouse hover tint — the
@@ -841,7 +902,7 @@ function ClickableDivider({ title, onClick }) {
841
902
  const [hovered, setHovered] = useState(false);
842
903
  return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
843
904
  }
844
- function TranscriptRow({ rowId, kind, text, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, thinkingFold, toolBackground, foldTerminalCommand, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, onToggleRow, onToggleStreamView, streamViewToggled, onOpenSubagent, onOpenFile, setRowRef, }) {
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, }) {
845
906
  const ref = React.useCallback((el) => {
846
907
  setRowRef(rowId, el);
847
908
  }, [setRowRef, rowId]);
@@ -878,7 +939,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
878
939
  // or per-row transcript expansion always wins and shows the full text.
879
940
  const streamPreview = streaming && !expanded && !isExpanded &&
880
941
  (streamViewToggled ? thinkingFold === 'full' : thinkingFold === 'preview');
881
- return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, addMargin: addMargin, streaming: streaming, preview: streamPreview,
942
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, textFull: textFull, addMargin: addMargin, streaming: streaming, preview: streamPreview,
882
943
  // Settled rows keep the fold-on-settle default and expand via
883
944
  // expandedRows/Ctrl+O; a live row is always preview or full.
884
945
  verbose: isExpanded || expanded || (streaming && !streamPreview), durationMs: durationMs, isSelected: isSelected, onClick: streaming ? streamViewOnClick : foldOnClick }) }));
@@ -907,7 +968,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
907
968
  startedAt: toolStartedAt,
908
969
  durationMs: toolDurationMs,
909
970
  };
910
- return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground, foldTerminalCommand: foldTerminalCommand, onClick: foldOnClick, onOpenFile: onOpenFile }) }));
971
+ return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantToolUseMessage, { tool: tool, addMargin: addMargin, verbose: isExpanded || expanded, isSelected: isSelected, isExpanded: isExpanded, footnote: toolFootnote, diffLayout: diffLayout, toolBackground: toolBackground, smoothReveal: smoothStreaming, fresh: fresh, foldTerminalCommand: foldTerminalCommand, onClick: foldOnClick, onOpenFile: onOpenFile }) }));
911
972
  }
912
973
  case 'notice':
913
974
  return (_jsx(Box, { marginTop: 1, ref: ref, children: _jsx(Divider, { title: ` ${text} ` }) }));
@@ -3,7 +3,7 @@ import type { ToolFileDiff } from '../dsh-adapter/channel.js';
3
3
  export { chalkFromToken } from '../cc/syntaxTheme.js';
4
4
  export { parseAnsiRuns, highlightLines } from '../cc/syntaxRuns.js';
5
5
  import type { ToolBackground } from '../tuiDisplayPrefs.js';
6
- export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground, }: {
6
+ export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground, reveal, }: {
7
7
  readonly diffs: readonly ToolFileDiff[];
8
8
  /** Content width available to the whole two-pane block (divider included). */
9
9
  readonly width: number;
@@ -11,5 +11,15 @@ export declare function SplitDiffView({ diffs, width, maxRows, verbose, toolBack
11
11
  readonly maxRows: number;
12
12
  readonly verbose: boolean;
13
13
  readonly toolBackground?: ToolBackground;
14
+ /**
15
+ * Smooth-streaming participation (the card owning this view computes
16
+ * eligibility): when present, the capped row list reveals line-by-line at
17
+ * the shared ~30fps cadence instead of painting as one block. The `+N
18
+ * lines` fold hint stays rendered throughout — it describes the cap, not
19
+ * the reveal.
20
+ */
21
+ readonly reveal?: {
22
+ readonly key: string;
23
+ };
14
24
  }): React.ReactNode;
15
25
  //# sourceMappingURL=SplitDiffView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SplitDiffView.d.ts","sourceRoot":"","sources":["../../../src/components/SplitDiffView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAO7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAuR3D,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,cAAuB,GACxB,EAAE;IACD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;CACzC,GAAG,KAAK,CAAC,SAAS,CAkFlB"}
1
+ {"version":3,"file":"SplitDiffView.d.ts","sourceRoot":"","sources":["../../../src/components/SplitDiffView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAO7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAwR3D,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,cAAuB,EACvB,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAA;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C,GAAG,KAAK,CAAC,SAAS,CAwFlB"}
@@ -11,6 +11,7 @@ export { chalkFromToken } from '../cc/syntaxTheme.js';
11
11
  export { parseAnsiRuns, highlightLines } from '../cc/syntaxRuns.js';
12
12
  import { getTheme } from '../theme.js';
13
13
  import { useTheme } from './design-system/ThemeProvider.js';
14
+ import { revealLinesOf } from './smoothReveal.js';
14
15
  /** Replace tabs so width math and alignment hold (pi convention). */
15
16
  const expandTabs = (text) => text.replaceAll('\t', ' ');
16
17
  /**
@@ -210,7 +211,7 @@ function PaneLine({ side, kind, width, tone, toolBackground, padLeft = false, })
210
211
  const prefix = padLeft ? ` ${marker}` : marker;
211
212
  return (_jsxs(Box, { width: width, flexShrink: 0, backgroundColor: backgroundColor, children: [_jsx(Text, { dimColor: true, backgroundColor: backgroundColor, children: `${prefix} ` }), side === undefined ? (_jsx(Text, { backgroundColor: backgroundColor, children: " " })) : (_jsx(Text, { wrap: "truncate", backgroundColor: backgroundColor, children: side.segments.map((segment, index) => segment.changed ? (_jsx(Text, { color: wordColor, bold: true, backgroundColor: backgroundColor, children: segment.text }, index)) : (_jsx(Text, { color: segment.color, backgroundColor: backgroundColor, children: segment.text }, index))) }))] }));
212
213
  }
213
- export function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground = 'none', }) {
214
+ export function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground = 'none', reveal, }) {
214
215
  const [hl, setHl] = React.useState(null);
215
216
  React.useEffect(() => {
216
217
  let mounted = true;
@@ -243,6 +244,12 @@ export function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground =
243
244
  const capped = verbose || totalRows <= maxRows || totalRows - maxRows === 1;
244
245
  const visible = capped ? rows : rows.slice(0, maxRows);
245
246
  const hidden = totalRows - visible.length;
247
+ // Smooth reveal reads happen during render (the owning card subscribes to
248
+ // the scheduler's version, so its re-render drives this view too).
249
+ const revealedRows = reveal !== undefined
250
+ ? revealLinesOf(reveal.key, visible.length, { enabled: true, active: true })
251
+ : visible.length;
252
+ const shown = revealedRows >= visible.length ? visible : visible.slice(0, revealedRows);
246
253
  const paneWidth = Math.max(20, Math.floor((width - 1) / 2));
247
254
  // Whole-hunk highlight per file (multi-line lexer state preserved);
248
255
  // only the visible rows merge syntax runs with word flags below.
@@ -253,7 +260,7 @@ export function SplitDiffView({ diffs, width, maxRows, verbose, toolBackground =
253
260
  next: highlightLines(expandTabs(diff.newText), language, hl, syntaxTheme, themeSig),
254
261
  };
255
262
  });
256
- return (_jsxs(Box, { flexDirection: "column", width: paneWidth * 2 + 1, children: [visible.map((row, index) => {
263
+ return (_jsxs(Box, { flexDirection: "column", width: paneWidth * 2 + 1, children: [shown.map((row, index) => {
257
264
  if ('separator' in row) {
258
265
  return (_jsx(Box, { width: paneWidth * 2 + 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: row.separator === '⋯' ? '⋯' : ` ${row.separator}` }) }, index));
259
266
  }
@@ -31,14 +31,19 @@ type DividerProps = {
31
31
  * A horizontal divider line, optionally with a title in the middle
32
32
  * (in the Claude Code visual language).
33
33
  *
34
- * The rule fills the width Yoga actually grants it: the stretched Box is
35
- * measured after layout (SearchBox pattern — a resize re-layouts without
36
- * any prop change, so measure on every commit; the setState is a no-op
37
- * when the width is unchanged) and the terminal column count only seeds
38
- * the first frame. A full-terminal-width rule nested in a narrower
34
+ * The rule fills the width Yoga actually grants it: the Box is measured
35
+ * after layout (SearchBox pattern — a resize re-layouts without any prop
36
+ * change, so measure on every commit) and the terminal column count only
37
+ * seeds the first frame. A full-terminal-width rule nested in a narrower
39
38
  * container (e.g., the transcript beside the 2-column timeline rail)
40
39
  * would otherwise wrap onto a second row.
41
40
  *
41
+ * Because the rendered rule width feeds back into the width Yoga grants
42
+ * the Box, the measurement negotiation is bounded per terminal-width
43
+ * generation (MAX_APPLIED_MEASUREMENTS below): a non-converging layout
44
+ * degrades to a frozen, truncated rule instead of tripping React's max
45
+ * update depth (error #185, a hard process crash).
46
+ *
42
47
  * @example
43
48
  * // ─────────── Title ───────────
44
49
  * <Divider title="Title" />
@@ -1 +1 @@
1
- {"version":3,"file":"Divider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/Divider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4C,MAAM,OAAO,CAAA;AAOhE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE3C,KAAK,YAAY,GAAG;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,KAAK,CAAA;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,IAAU,EACV,OAAW,EACX,KAAK,GACN,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAsChC"}
1
+ {"version":3,"file":"Divider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/Divider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4C,MAAM,OAAO,CAAA;AAOhE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE3C,KAAK,YAAY,GAAG;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,KAAK,CAAA;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AASD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,IAAU,EACV,OAAW,EACX,KAAK,GACN,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAqEhC"}
@@ -5,18 +5,29 @@ import Text from '../../ink/components/Text.js';
5
5
  import measureElement from '../../ink/measure-element.js';
6
6
  import { stringWidth } from '../../ink/stringWidth.js';
7
7
  import { useTerminalSize } from '../../ink/hooks/use-terminal-size.js';
8
+ // Upper bound on distinct measurements applied per terminal-width
9
+ // generation. Converging layouts apply one or two; a non-converging
10
+ // context (content-sized Box, or a sibling negotiating space the same
11
+ // way) would keep changing the measured width once per commit forever,
12
+ // so freeze well before the reconciler's 50-nested-update limit.
13
+ const MAX_APPLIED_MEASUREMENTS = 8;
8
14
  /**
9
15
  * A horizontal divider line, optionally with a title in the middle
10
16
  * (in the Claude Code visual language).
11
17
  *
12
- * The rule fills the width Yoga actually grants it: the stretched Box is
13
- * measured after layout (SearchBox pattern — a resize re-layouts without
14
- * any prop change, so measure on every commit; the setState is a no-op
15
- * when the width is unchanged) and the terminal column count only seeds
16
- * the first frame. A full-terminal-width rule nested in a narrower
18
+ * The rule fills the width Yoga actually grants it: the Box is measured
19
+ * after layout (SearchBox pattern — a resize re-layouts without any prop
20
+ * change, so measure on every commit) and the terminal column count only
21
+ * seeds the first frame. A full-terminal-width rule nested in a narrower
17
22
  * container (e.g., the transcript beside the 2-column timeline rail)
18
23
  * would otherwise wrap onto a second row.
19
24
  *
25
+ * Because the rendered rule width feeds back into the width Yoga grants
26
+ * the Box, the measurement negotiation is bounded per terminal-width
27
+ * generation (MAX_APPLIED_MEASUREMENTS below): a non-converging layout
28
+ * degrades to a frozen, truncated rule instead of tripping React's max
29
+ * update depth (error #185, a hard process crash).
30
+ *
20
31
  * @example
21
32
  * // ─────────── Title ───────────
22
33
  * <Divider title="Title" />
@@ -25,13 +36,46 @@ export function Divider({ width, color, char = '─', padding = 0, title, }) {
25
36
  const { columns } = useTerminalSize();
26
37
  const [measuredWidth, setMeasuredWidth] = useState(null);
27
38
  const boxRef = useRef(null);
39
+ // Measurement feedback guard: the rule width rendered from
40
+ // `measuredWidth` feeds back into the width Yoga grants the Box, so
41
+ // the measured value can drift or ping-pong without ever converging —
42
+ // one setState per commit then trips React's max update depth (error
43
+ // #185) and kills the process. Apply at most MAX_APPLIED_MEASUREMENTS
44
+ // distinct widths per terminal-width generation and freeze once a
45
+ // value repeats (A→B→A). truncate-end keeps an over-wide frozen rule
46
+ // on one row, so freezing degrades gracefully; a resize (columns
47
+ // change) reopens the negotiation.
48
+ const negotiation = useRef({ columns, applied: [], frozen: false });
28
49
  useLayoutEffect(() => {
29
50
  const node = boxRef.current;
30
51
  if (!node)
31
52
  return;
53
+ const state = negotiation.current;
54
+ if (state.columns !== columns) {
55
+ state.columns = columns;
56
+ state.applied = [];
57
+ state.frozen = false;
58
+ // The current Yoga layout was rendered from the previous terminal
59
+ // width. Clear it and let the next render seed from the new columns;
60
+ // measuring this stale pass would keep a grown terminal permanently
61
+ // pinned to its old narrow width.
62
+ setMeasuredWidth(null);
63
+ return;
64
+ }
65
+ if (state.frozen)
66
+ return;
32
67
  const w = measureElement(node).width;
33
- if (w > 0)
34
- setMeasuredWidth(prev => (prev === w ? prev : w));
68
+ if (w <= 0)
69
+ return;
70
+ const applied = state.applied;
71
+ if (w === applied[applied.length - 1])
72
+ return;
73
+ if (applied.includes(w) || applied.length >= MAX_APPLIED_MEASUREMENTS) {
74
+ state.frozen = true;
75
+ return;
76
+ }
77
+ applied.push(w);
78
+ setMeasuredWidth(w);
35
79
  });
36
80
  const available = measuredWidth ?? columns;
37
81
  const lineWidth = Math.max(0, Math.min(available, width ?? columns) - padding);
@@ -2,6 +2,11 @@ import React from 'react';
2
2
  import type { ClickEvent } from '../../ink/events/click-event.js';
3
3
  type Props = {
4
4
  thinking: string;
5
+ /** The FULL un-revealed text (reasoning rows under smooth streaming):
6
+ * `thinking` carries the revealed slice the expanded body paints, while
7
+ * the live preview ticker must follow the newest ARRIVED content — never
8
+ * a lagging reveal. Falls back to `thinking`. */
9
+ textFull?: string;
5
10
  /** Adds the top margin between messages (CC: addMargin). */
6
11
  addMargin: boolean;
7
12
  /** Show the full text (Ctrl+O, per-row expansion, or live click toggle). */
@@ -29,6 +34,6 @@ type Props = {
29
34
  * (`⚓ Thinking · 12s …`) — dsh-tui's take on making thinking time visible in
30
35
  * the transcript.
31
36
  */
32
- export declare function AssistantThinkingMessage({ thinking, addMargin, verbose, streaming, preview, durationMs, isSelected, onClick, }: Props): React.ReactNode;
37
+ export declare function AssistantThinkingMessage({ thinking, textFull, addMargin, verbose, streaming, preview, durationMs, isSelected, onClick, }: Props): React.ReactNode;
33
38
  export {};
34
39
  //# sourceMappingURL=AssistantThinkingMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantThinkingMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantThinkingMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAczB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAUjE,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;4CAEwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;8EAE0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CAClC,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAiB,EACjB,OAAe,EACf,UAAU,EACV,UAAkB,EAClB,OAAO,GACR,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAkIzB"}
1
+ {"version":3,"file":"AssistantThinkingMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantThinkingMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAczB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAUjE,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB;;;sDAGkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;4CAEwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;8EAE0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CAClC,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAiB,EACjB,OAAe,EACf,UAAU,EACV,UAAkB,EAClB,OAAO,GACR,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAuIzB"}
@@ -25,9 +25,13 @@ const PREVIEW_ROWS = 3;
25
25
  * (`⚓ Thinking · 12s …`) — dsh-tui's take on making thinking time visible in
26
26
  * the transcript.
27
27
  */
28
- export function AssistantThinkingMessage({ thinking, addMargin, verbose, streaming = false, preview = false, durationMs, isSelected = false, onClick, }) {
28
+ export function AssistantThinkingMessage({ thinking, textFull, addMargin, verbose, streaming = false, preview = false, durationMs, isSelected = false, onClick, }) {
29
29
  if (!thinking)
30
30
  return null;
31
+ // The preview ticker tracks the newest ARRIVED line (smooth streaming must
32
+ // not lag it behind the reveal); the expanded body below paints `thinking`
33
+ // — the revealed slice under smooth streaming, the full text otherwise.
34
+ const tickerText = textFull ?? thinking;
31
35
  // Spinner frame (80ms cadence, only while the reasoning is still
32
36
  // streaming — same pattern as BtwPanel's answering spinner).
33
37
  const [frame, setFrame] = React.useState(0);
@@ -64,7 +68,7 @@ export function AssistantThinkingMessage({ thinking, addMargin, verbose, streami
64
68
  // from the start (leading ellipsis) so the newest tokens — which grow
65
69
  // at the line's end — stay visible while the line is longer than the
66
70
  // width.
67
- const lines = thinking.split('\n');
71
+ const lines = tickerText.split('\n');
68
72
  const visible = lines.slice(-PREVIEW_ROWS);
69
73
  const clipped = lines.length > visible.length;
70
74
  // Pad with single spaces — an empty-string Text renders with zero
@@ -45,6 +45,18 @@ type Props = {
45
45
  * `+N lines` hint; verbose/expanded cards render the full title.
46
46
  */
47
47
  foldTerminalCommand?: boolean;
48
+ /**
49
+ * Smooth streaming reveal (settings `dsh-tui.smoothStreaming`): the card
50
+ * BODY (diff hunks / write content — model-authored prose, not tool
51
+ * output) paints through an even ~30fps line reveal when it first appears,
52
+ * instead of one jarring block. Only the pending CALL view animates; the
53
+ * settled result view paints complete (real output is progress, not
54
+ * prose), and so do replayed cards.
55
+ */
56
+ smoothReveal?: boolean;
57
+ /** Live-arrived row (channel `fresh`): gates reveal participation —
58
+ * replayed cards must paint complete. */
59
+ fresh?: boolean;
48
60
  };
49
61
  export declare function toolNameColor(raw: string): keyof Theme;
50
62
  /**
@@ -53,6 +65,6 @@ export declare function toolNameColor(raw: string): keyof Theme;
53
65
  * output, read content — instead of the raw result dump (mirroring Claude Code's `AssistantToolUseMessage.tsx` + the dsh-tools presentation views the
54
66
  * channel captures per call).
55
67
  */
56
- export declare function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected, isExpanded, onClick, footnote, diffLayout, toolBackground, onOpenFile, foldTerminalCommand, }: Props): React.ReactNode;
68
+ export declare function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected, isExpanded, onClick, footnote, diffLayout, toolBackground, onOpenFile, foldTerminalCommand, smoothReveal, fresh, }: Props): React.ReactNode;
57
69
  export {};
58
70
  //# sourceMappingURL=AssistantToolUseMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantToolUseMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantToolUseMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAA8C,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAMvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACzC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B,CAAA;AAsFD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,CAKtD;AA+OD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,UAAkB,EAClB,UAAkB,EAClB,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,cAAuB,EACvB,UAAU,EACV,mBAA2B,GAC5B,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CA6MzB"}
1
+ {"version":3,"file":"AssistantToolUseMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/messages/AssistantToolUseMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAA8C,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAMvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAGjE,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACzC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;8CAC0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAsFD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,CAKtD;AA+OD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,UAAkB,EAClB,UAAkB,EAClB,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,cAAuB,EACvB,UAAU,EACV,mBAA2B,EAC3B,YAAoB,EACpB,KAAa,GACd,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CAiOzB"}
@@ -9,6 +9,7 @@ import { SplitDiffView } from '../SplitDiffView.js';
9
9
  import { SyntaxText } from '../SyntaxText.js';
10
10
  import { useTooltip } from '../Tooltip.js';
11
11
  import { formatDuration } from '../../cc/format.js';
12
+ import { getRevealVersion, revealLinesOf, snapReveal, subscribeReveal } from '../smoothReveal.js';
12
13
  /** Tool display names: DSH emits lowercase tool ids (`bash`); Claude Code
13
14
  * shows capitalized names (`Bash`). Map the common ones, fall back to the
14
15
  * id with its first letter uppercased. */
@@ -269,7 +270,11 @@ function HeaderTitle({ name, title, isTerminal, folded, displayArgs, argsLanguag
269
270
  * output, read content — instead of the raw result dump (mirroring Claude Code's `AssistantToolUseMessage.tsx` + the dsh-tools presentation views the
270
271
  * channel captures per call).
271
272
  */
272
- export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected = false, isExpanded = false, onClick, footnote, diffLayout = 'auto', toolBackground = 'none', onOpenFile, foldTerminalCommand = false, }) {
273
+ export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected = false, isExpanded = false, onClick, footnote, diffLayout = 'auto', toolBackground = 'none', onOpenFile, foldTerminalCommand = false, smoothReveal = false, fresh = false, }) {
274
+ // Reveal-frame wakeups: the shared scheduler bumps its version, which
275
+ // re-renders this card even while MemoRow props hold still (the reveal
276
+ // lives outside React — see smoothReveal.ts).
277
+ React.useSyncExternalStore(subscribeReveal, getRevealVersion);
273
278
  const isRunning = tool.status === 'running';
274
279
  const isError = tool.status === 'error';
275
280
  const displayArgs = verbose ? tool.argsFull ?? tool.argsText : tool.argsText;
@@ -334,6 +339,21 @@ export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected =
334
339
  // long error body must not be the reason it disappears.
335
340
  const lines = capLines(body, cap, verbose);
336
341
  const rendered = footnote === undefined ? lines : [...lines, { text: footnote, tone: 'hint' }];
342
+ // Smooth reveal (line-unit, pending CALL body only): model-authored prose
343
+ // (diff hunks, write content) flows in at ~30fps; the settled RESULT view,
344
+ // error bodies, verbose/expanded cards, and replayed (non-fresh) cards all
345
+ // paint complete. `snapReveal` on every non-revealable render retires a
346
+ // cursor the moment its card stops qualifying (result arrived, user
347
+ // expanded) — idempotent, safe during render.
348
+ const revealKey = `tool:${tool.callId}`;
349
+ const revealable = smoothReveal && !isError && isRunning && view !== undefined &&
350
+ tool.resultView === undefined && !verbose && !isExpanded && fresh;
351
+ if (!revealable)
352
+ snapReveal(revealKey);
353
+ const revealedLineCount = revealable
354
+ ? revealLinesOf(revealKey, rendered.length, { enabled: true, active: true })
355
+ : rendered.length;
356
+ const shownLines = revealedLineCount >= rendered.length ? rendered : rendered.slice(0, revealedLineCount);
337
357
  // Nested split-diff context panes must also yield to interaction highlights.
338
358
  // `none` leaves them transparent so the selected/expanded root shows through.
339
359
  const ordinaryToolBackground = isSelected || isExpanded ? 'none' : toolBackground;
@@ -355,7 +375,7 @@ export function AssistantToolUseMessage({ tool, addMargin, verbose, isSelected =
355
375
  return (_jsx(Box, { ref: viewportRef, flexDirection: "row", justifyContent: "space-between", marginTop: addMargin ? 1 : 0, width: "100%", onClick: onClick,
356
376
  // Only selection paints a highlight; the configured treatment applies
357
377
  // to an ordinary card. Diff line tints stay - they are content, not chrome.
358
- backgroundColor: isSelected ? 'messageActionsBackground' : hoverTint ? 'toolCardBackground' : ordinaryBackground, onMouseEnter: interactive ? () => setHovered(true) : undefined, onMouseLeave: interactive ? () => setHovered(false) : undefined, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", minWidth: minWidth, children: [_jsx(ToolUseLoader, { shouldAnimate: isRunning, isUnresolved: isRunning, isError: isError, toolName: tool.name }), _jsx(HeaderTitle, { name: name, title: headerTitle, isTerminal: headerIsTerminal, folded: foldedHeader, displayArgs: displayArgs, argsLanguage: argsLanguage, nameColor: toolNameColor(tool.name), filePath: filePath, onOpenFile: onOpenFile }), !isRunning && (_jsx(Box, { flexWrap: "nowrap", children: _jsx(Text, { dimColor: !hovered, children: elapsedText }) })), hovered && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, children: isExpanded ? '▴' : '▾' }) }))] }), useSplitDiff && view?.card === 'diff' ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: GUTTER_FIRST }) }), _jsx(SplitDiffView, { diffs: view.diffs, width: columns - 4, maxRows: DIFF_BODY_MAX_LINES, verbose: verbose, toolBackground: ordinaryToolBackground })] })) : (rendered.map((line, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { color: line.tone === 'add'
378
+ backgroundColor: isSelected ? 'messageActionsBackground' : hoverTint ? 'toolCardBackground' : ordinaryBackground, onMouseEnter: interactive ? () => setHovered(true) : undefined, onMouseLeave: interactive ? () => setHovered(false) : undefined, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", minWidth: minWidth, children: [_jsx(ToolUseLoader, { shouldAnimate: isRunning, isUnresolved: isRunning, isError: isError, toolName: tool.name }), _jsx(HeaderTitle, { name: name, title: headerTitle, isTerminal: headerIsTerminal, folded: foldedHeader, displayArgs: displayArgs, argsLanguage: argsLanguage, nameColor: toolNameColor(tool.name), filePath: filePath, onOpenFile: onOpenFile }), !isRunning && (_jsx(Box, { flexWrap: "nowrap", children: _jsx(Text, { dimColor: !hovered, children: elapsedText }) })), hovered && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, children: isExpanded ? '▴' : '▾' }) }))] }), useSplitDiff && view?.card === 'diff' ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: GUTTER_FIRST }) }), _jsx(SplitDiffView, { diffs: view.diffs, width: columns - 4, maxRows: DIFF_BODY_MAX_LINES, verbose: verbose, toolBackground: ordinaryToolBackground, reveal: revealable ? { key: `${revealKey}:split` } : undefined })] })) : (shownLines.map((line, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { color: line.tone === 'add'
359
379
  ? 'diffAddedWord'
360
380
  : line.tone === 'del'
361
381
  ? 'diffRemovedWord'