@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.
- package/lib/types/components/MessageList.d.ts +5 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +70 -9
- package/lib/types/components/SplitDiffView.d.ts +11 -1
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +9 -2
- package/lib/types/components/design-system/Divider.d.ts +10 -5
- package/lib/types/components/design-system/Divider.d.ts.map +1 -1
- package/lib/types/components/design-system/Divider.js +51 -7
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +6 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +6 -2
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +13 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +22 -2
- package/lib/types/components/smoothReveal.d.ts +97 -0
- package/lib/types/components/smoothReveal.d.ts.map +1 -0
- package/lib/types/components/smoothReveal.js +279 -0
- package/lib/types/dsh-adapter/channel.d.ts +17 -0
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +16 -1
- package/lib/types/dsh-adapter/index.d.ts +5 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +1 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +16 -0
- package/lib/types/ink/parse-keypress.d.ts +18 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +178 -7
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +8 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +1 -1
- 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":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAA;AAEvC,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAE3E,OAAO,KAAK,EAAE,OAAO,EAAsD,MAAM,2BAA2B,CAAA;AAC5G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAgB/C,OAAO,EAAe,KAAK,gBAAgB,EAAqB,MAAM,yBAAyB,CAAA;AAC/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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: [
|
|
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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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;
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
34
|
-
|
|
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,
|
|
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 =
|
|
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;
|
|
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 })] })) : (
|
|
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'
|