@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/bin/dsh-tui.js +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +4 -2
- package/lib/types/components/ActivityPicker.d.ts +4 -1
- package/lib/types/components/ActivityPicker.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.js +2 -2
- package/lib/types/components/BtwPanel.d.ts.map +1 -1
- package/lib/types/components/BtwPanel.js +1 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
- package/lib/types/components/Chat/SubagentMessage.js +11 -3
- package/lib/types/components/CommandSuggestions.d.ts +21 -4
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +41 -14
- package/lib/types/components/EffortSlider.d.ts +4 -1
- package/lib/types/components/EffortSlider.d.ts.map +1 -1
- package/lib/types/components/EffortSlider.js +4 -2
- package/lib/types/components/ExtensionDialog.js +6 -2
- package/lib/types/components/FileSuggestions.d.ts +26 -5
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +53 -16
- package/lib/types/components/GoalTodoPanel.d.ts +26 -2
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +86 -17
- package/lib/types/components/HelpMenu.d.ts +7 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +12 -4
- package/lib/types/components/HistorySearchDialog.d.ts +4 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +2 -2
- package/lib/types/components/LangPicker.d.ts +14 -0
- package/lib/types/components/LangPicker.d.ts.map +1 -0
- package/lib/types/components/LangPicker.js +17 -0
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +3 -1
- package/lib/types/components/LogoV2.d.ts +5 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +12 -3
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +12 -3
- package/lib/types/components/MessageList.d.ts +43 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +553 -41
- package/lib/types/components/ModelPicker.d.ts +4 -1
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +2 -2
- package/lib/types/components/PermissionsPicker.d.ts +21 -0
- package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
- package/lib/types/components/PermissionsPicker.js +27 -0
- package/lib/types/components/PlanPicker.d.ts +15 -0
- package/lib/types/components/PlanPicker.d.ts.map +1 -0
- package/lib/types/components/PlanPicker.js +18 -0
- package/lib/types/components/PresetPicker.d.ts +4 -1
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +4 -3
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +273 -50
- package/lib/types/components/RewindPicker.d.ts +12 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +6 -4
- package/lib/types/components/ScrollbarGutter.d.ts +28 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
- package/lib/types/components/ScrollbarGutter.js +68 -0
- package/lib/types/components/Select.d.ts +8 -1
- package/lib/types/components/Select.d.ts.map +1 -1
- package/lib/types/components/Select.js +4 -2
- package/lib/types/components/SkillsPicker.d.ts +4 -1
- package/lib/types/components/SkillsPicker.d.ts.map +1 -1
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SubagentCard.d.ts +2 -1
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +6 -1
- package/lib/types/components/SubagentDashboard.d.ts +4 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
- package/lib/types/components/SubagentDashboard.js +9 -1
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +5 -4
- package/lib/types/components/SuggestionCard.d.ts +63 -0
- package/lib/types/components/SuggestionCard.d.ts.map +1 -0
- package/lib/types/components/SuggestionCard.js +77 -0
- package/lib/types/components/ThemePicker.d.ts +4 -1
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +2 -2
- package/lib/types/components/ThinkingToggle.d.ts +4 -1
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +2 -2
- package/lib/types/components/TimelineRail.d.ts +53 -0
- package/lib/types/components/TimelineRail.d.ts.map +1 -0
- package/lib/types/components/TimelineRail.js +190 -0
- package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
- package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
- package/lib/types/components/WorkspaceFlowPicker.js +4 -2
- package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
- package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
- package/lib/types/components/WorkspaceMenuPicker.js +19 -0
- package/lib/types/components/WorkspacePicker.d.ts +4 -1
- package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
- package/lib/types/components/WorkspacePicker.js +2 -2
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +5 -1
- package/lib/types/components/design-system/ListItem.d.ts +8 -1
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +8 -3
- package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
- package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantTextMessage.js +6 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
- package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +13 -2
- package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +4 -2
- package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
- package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
- package/lib/types/components/trajectory/HotspotView.js +8 -2
- package/lib/types/components/trajectory/Ledger.d.ts +9 -1
- package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
- package/lib/types/components/trajectory/Ledger.js +23 -5
- package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
- package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
- package/lib/types/components/trajectory/WaveBand.js +7 -2
- package/lib/types/dsh-adapter/channel.d.ts +27 -6
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +432 -66
- package/lib/types/dsh-adapter/contract.d.ts +58 -18
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +124 -36
- package/lib/types/dsh-adapter/index.d.ts +10 -2
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +10 -13
- package/lib/types/dsh-adapter/plugin.d.ts +0 -9
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +136 -6
- package/lib/types/i18n.d.ts +194 -16
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +73 -10
- package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
- package/lib/types/ink/components/AlternateScreen.js +13 -1
- package/lib/types/ink/components/App.d.ts +12 -1
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +101 -4
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +2 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +170 -91
- package/lib/types/ink/constants.d.ts +9 -0
- package/lib/types/ink/constants.d.ts.map +1 -1
- package/lib/types/ink/constants.js +9 -0
- package/lib/types/ink/events/click-event.d.ts +7 -16
- package/lib/types/ink/events/click-event.d.ts.map +1 -1
- package/lib/types/ink/events/click-event.js +16 -19
- package/lib/types/ink/events/event-handlers.d.ts +11 -1
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +2 -0
- package/lib/types/ink/events/input-event.d.ts +9 -0
- package/lib/types/ink/events/input-event.d.ts.map +1 -1
- package/lib/types/ink/events/input-event.js +4 -0
- package/lib/types/ink/events/pointer-event.d.ts +61 -0
- package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
- package/lib/types/ink/events/pointer-event.js +61 -0
- package/lib/types/ink/events/wheel-event.d.ts +17 -0
- package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
- package/lib/types/ink/events/wheel-event.js +20 -0
- package/lib/types/ink/geometry-trace.d.ts +51 -0
- package/lib/types/ink/geometry-trace.d.ts.map +1 -0
- package/lib/types/ink/geometry-trace.js +83 -0
- package/lib/types/ink/hit-test.d.ts +56 -1
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +146 -8
- package/lib/types/ink/ink.d.ts +58 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +268 -52
- package/lib/types/ink/node-cache.d.ts +5 -0
- package/lib/types/ink/node-cache.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.d.ts +8 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +101 -12
- package/lib/types/ink/render-node-to-output.d.ts +16 -3
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +144 -25
- package/lib/types/ink/selection.d.ts +29 -4
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +41 -5
- package/lib/types/ink/timeline-rail.d.ts +138 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -0
- package/lib/types/ink/timeline-rail.js +179 -0
- package/lib/types/ink/wrap-text.d.ts.map +1 -1
- package/lib/types/ink/wrap-text.js +47 -5
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +819 -337
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +56 -29
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +64 -22
- package/lib/types/screens/StatusLine.d.ts +2 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +17 -1
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +81 -14
- package/lib/types/screens/chatOverlay.d.ts +221 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -0
- package/lib/types/screens/chatOverlay.js +114 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +64 -10
- package/lib/types/tuiDisplayPrefs.d.ts +15 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +9 -0
- package/lib/types/utils/debug.d.ts +10 -0
- package/lib/types/utils/debug.d.ts.map +1 -1
- package/lib/types/utils/debug.js +24 -0
- package/lib/types/utils/fileSuggestions.d.ts +19 -0
- package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
- package/lib/types/utils/fileSuggestions.js +40 -0
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +3 -1
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/package.json +69 -66
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
3
|
import { t } from '../i18n.js';
|
|
4
4
|
import { Box, Text, useTerminalSize } from '../ui.js';
|
|
5
5
|
import { Divider } from './design-system/Divider.js';
|
|
@@ -9,6 +9,7 @@ import { AssistantThinkingMessage } from './messages/AssistantThinkingMessage.js
|
|
|
9
9
|
import { AssistantToolUseMessage } from './messages/AssistantToolUseMessage.js';
|
|
10
10
|
import { SubagentMessage } from './Chat/SubagentMessage.js';
|
|
11
11
|
import { isMinimalMode } from '../minimalMode.js';
|
|
12
|
+
import { noteFrameCause, noteListGeometry } from '../ink/geometry-trace.js';
|
|
12
13
|
import { InterruptedByUser } from './InterruptedByUser.js';
|
|
13
14
|
import { LogoV2 } from './LogoV2.js';
|
|
14
15
|
import { StreamingMarkdown } from './StreamingMarkdown.js';
|
|
@@ -16,6 +17,7 @@ import { MessageMetadata } from './messages/MessageMetadata.js';
|
|
|
16
17
|
import { stripNarration } from '../utils/narration.js';
|
|
17
18
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
18
19
|
import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
20
|
+
import { clipPreview } from '../ink/timeline-rail.js';
|
|
19
21
|
/**
|
|
20
22
|
* Transcript rows rendered in the Claude Code visual language: user prompts
|
|
21
23
|
* on a grey bubble with a `❯` pointer, assistant text with a `●` bullet and
|
|
@@ -25,8 +27,14 @@ import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
|
25
27
|
* rows; `selectedId` highlights the selected row.
|
|
26
28
|
*/
|
|
27
29
|
/** Render cap for very long sessions (CC's MAX_MESSAGES_WITHOUT_VIRTUALIZATION
|
|
28
|
-
* equivalent): older rows fold behind a Divider until Ctrl+E expands them.
|
|
29
|
-
|
|
30
|
+
* equivalent): older rows fold behind a Divider until Ctrl+E expands them.
|
|
31
|
+
* 120 (was 300): opening a long session paints the whole cap into the
|
|
32
|
+
* main-screen scrollback (historyPaint), and each row's first markdown
|
|
33
|
+
* lex + wrap costs ~2-5ms — 300 rows saturated the main thread for ~6s
|
|
34
|
+
* on open (measured, 800-row inline session). 120 rows ≈ 4-5 screens of
|
|
35
|
+
* paint (<1s) with the rest behind the show-previous divider (CC parity:
|
|
36
|
+
* the transcript is a viewport, not a printout; load-earlier restores). */
|
|
37
|
+
const MAX_RENDERED_ROWS = 120;
|
|
30
38
|
// --- layout virtualization constants -------------------------------------
|
|
31
39
|
// Offscreen rows render as fixed-height spacers whose heights come from the
|
|
32
40
|
// previous commit's Yoga layout, so the pure-JS Yoga engine never walks
|
|
@@ -39,23 +47,163 @@ const DEFAULT_ROW_HEIGHT = 2;
|
|
|
39
47
|
/** Cold-start estimate of the header block above the rows; corrected by the
|
|
40
48
|
* first layout measurement. */
|
|
41
49
|
const DEFAULT_HEADER_LINES = 14;
|
|
42
|
-
|
|
50
|
+
/** Stable fallbacks for the stream-fold props: verify/repro harnesses and
|
|
51
|
+
* embedders render MessageList with prop sets that predate them, and the
|
|
52
|
+
* render must not throw (same rule as Chat's stubbed channel APIs). Module
|
|
53
|
+
* scope keeps the identities stable so MemoRow's shallow compare and the
|
|
54
|
+
* toggle callback's deps never churn. */
|
|
55
|
+
const NO_STREAM_FOLDED = new Set();
|
|
56
|
+
const NOOP_TOGGLE_STREAM_FOLD = (_rowId) => { };
|
|
57
|
+
/**
|
|
58
|
+
* Per-kind layout signature PARTS: the O(1) identity of every input that
|
|
59
|
+
* decides a row's rendered HEIGHT (see sigRef in MessageList). Fields are
|
|
60
|
+
* scoped to the row's own renderer — a global flat signature
|
|
61
|
+
* over-invalidates (a diffLayout switch must not drop user-message
|
|
62
|
+
* heights). Text uses length as the proxy: full-text hashing per row per
|
|
63
|
+
* frame would defeat virtualization's budget, and a same-length miss only
|
|
64
|
+
* degrades to the previous behavior.
|
|
65
|
+
*
|
|
66
|
+
* Returns a PARTS VECTOR instead of a joined string: the caller compares
|
|
67
|
+
* slot-by-slot against the cached vector (all primitives), allocating only
|
|
68
|
+
* when a row's inputs actually changed. Building a joined string per row
|
|
69
|
+
* per render was O(rows) allocation churn on every scroll tick — the GC
|
|
70
|
+
* share of the long-session scroll profile.
|
|
71
|
+
*
|
|
72
|
+
* The vector is a MODULE-LEVEL SCRATCH BUFFER: single-threaded synchronous
|
|
73
|
+
* render makes reuse safe, and the unchanged case (the overwhelming
|
|
74
|
+
* majority) allocates nothing. Callers that retain the vector must copy
|
|
75
|
+
* (parts.slice()).
|
|
76
|
+
*/
|
|
77
|
+
const signatureScratch = [];
|
|
78
|
+
function signatureParts(row, columns, expanded, expandedRows, streamFolded, thinkingVisible, thinkingFold, diffLayout, model, failureHintRowId, failureHint) {
|
|
79
|
+
signatureScratch.length = 0;
|
|
80
|
+
// Universal height inputs: width reflows every row; kind switches height
|
|
81
|
+
// semantics wholesale; text length drives wrapping.
|
|
82
|
+
signatureScratch.push(columns, row.kind, row.text?.length ?? 0);
|
|
83
|
+
switch (row.kind) {
|
|
84
|
+
case 'assistant':
|
|
85
|
+
// Streaming vs settled swaps renderers; Ctrl+O/per-row expand adds the
|
|
86
|
+
// metadata row (model only renders expanded — keeps an idle /model
|
|
87
|
+
// switch from touching settled rows).
|
|
88
|
+
signatureScratch.push(row.streaming === true, expanded, expandedRows.has(row.id), expanded ? model : '');
|
|
89
|
+
break;
|
|
90
|
+
case 'reasoning':
|
|
91
|
+
// thinkingFold (preview vs full) and the visibility filter change the
|
|
92
|
+
// folded card's height; streaming shows verbose live unless the user
|
|
93
|
+
// clicked it folded (streamFolded collapses to the ticker/header).
|
|
94
|
+
signatureScratch.push(row.streaming === true, expanded, expandedRows.has(row.id), streamFolded.has(row.id), thinkingVisible, thinkingFold);
|
|
95
|
+
break;
|
|
96
|
+
case 'tool': {
|
|
97
|
+
const tool = row.tool;
|
|
98
|
+
signatureScratch.push(expanded, expandedRows.has(row.id), diffLayout, tool?.status ?? '', tool?.resultText?.length ?? 0, tool?.resultFull?.length ?? 0, tool?.errorText?.length ?? 0, row.id === failureHintRowId ? failureHint ?? '' : '');
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'subagent':
|
|
102
|
+
// 卡片高度输入:running→settled 折叠 waterfall+tool 行(5→1 行),
|
|
103
|
+
// failed 增加 error 行。缺这些字段的话 offscreen 结算后 cached
|
|
104
|
+
// height 永不过期 → 滚回 blank band / 滚不到底。
|
|
105
|
+
signatureScratch.push(row.subagent?.status ?? '', row.subagent?.toolCalls.length ?? 0, row.subagent?.outputLines.length ?? 0, row.subagent?.error?.length ?? 0);
|
|
106
|
+
break;
|
|
107
|
+
case 'compact':
|
|
108
|
+
// Folded one-liner vs full summary text.
|
|
109
|
+
signatureScratch.push(expanded, expandedRows.has(row.id));
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
// user / notice / interrupt / local / local-output: height follows
|
|
113
|
+
// text + columns alone (selection/background never change height).
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
return signatureScratch;
|
|
117
|
+
}
|
|
118
|
+
export function MessageList({ rows, expanded, expandedRows, selectedId, onToggleRow, streamFoldedRows = NO_STREAM_FOLDED, onToggleStreamFold = NOOP_TOGGLE_STREAM_FOLD, model, diffLayout = 'auto', thinkingFold = 'preview', toolBackground = 'none', activityFrames, showAll, onToggleAll, onLoadOlder, thinkingVisible = true, historyPaintEnabled = true, registerRowRef, scrollHandle, forceMountRowId, newSinceRowId, onUnseenCount, onTimeline, failureHintRowId, failureHint, onOpenSubagent, }) {
|
|
43
119
|
const hiddenCount = rows.length - MAX_RENDERED_ROWS;
|
|
44
120
|
// The thinking filter runs BEFORE virtualization so window indices line up.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
121
|
+
//
|
|
122
|
+
// Fingerprint memo: every scroll tick re-rendered this pipeline even when
|
|
123
|
+
// nothing changed — slice + filter allocate a fresh rows-length array and
|
|
124
|
+
// the margins pre-pass allocates a Map with one entry per row (3200-row
|
|
125
|
+
// session ⇒ ~200KB churn per 16ms tick, the GC share of the scroll
|
|
126
|
+
// profile). The inputs are all identity-stable across ticks: channel.rows
|
|
127
|
+
// is a live in-place array (identity changes only on rewind/new session),
|
|
128
|
+
// showAll/thinkingVisible are React state. Rows APPENDED in place keep the
|
|
129
|
+
// identity — the cache must key on rows.length too (streaming appends).
|
|
130
|
+
const visibleRowsCacheRef = React.useRef(null);
|
|
131
|
+
/** Generation counter for the visibleRows cache (timeline memo key). */
|
|
132
|
+
const visGenRef = React.useRef(0);
|
|
133
|
+
const visibleCache = visibleRowsCacheRef.current;
|
|
134
|
+
// Streaming-bit fingerprint: in-place `streaming = false` writes (turn
|
|
135
|
+
// settle) are invisible to the rows-identity/length key above, but an
|
|
136
|
+
// assistant row that settles with EMPTY text crosses the empty-assistant
|
|
137
|
+
// filter boundary (visible-while-streaming → filtered-when-settled).
|
|
138
|
+
// Allocation-free scan; rebuild only when a bit actually flipped.
|
|
139
|
+
let streamBitsSame = visibleCache !== null && visibleCache.streamBits.length === rows.length;
|
|
140
|
+
if (streamBitsSame) {
|
|
141
|
+
const bits = visibleCache.streamBits;
|
|
142
|
+
for (let i = 0; i < rows.length; i++) {
|
|
143
|
+
if (bits[i] !== (rows[i].streaming === true ? 1 : 0)) {
|
|
144
|
+
streamBitsSame = false;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
57
147
|
}
|
|
58
148
|
}
|
|
149
|
+
if (visibleCache === null ||
|
|
150
|
+
visibleCache.rows !== rows ||
|
|
151
|
+
visibleCache.rowsLength !== rows.length ||
|
|
152
|
+
visibleCache.showAll !== (showAll || hiddenCount <= 0) ||
|
|
153
|
+
visibleCache.thinkingVisible !== thinkingVisible ||
|
|
154
|
+
!streamBitsSame) {
|
|
155
|
+
const sliced = showAll || hiddenCount <= 0
|
|
156
|
+
? rows
|
|
157
|
+
: rows.slice(hiddenCount);
|
|
158
|
+
// Empty settled assistant rows (PR #383's duplicate-dot bug): when the
|
|
159
|
+
// model calls a tool without producing text, the assistant/message event
|
|
160
|
+
// carries empty text — rendered as a lone `●` bullet dangling above the
|
|
161
|
+
// tool card. Filter them BEFORE virtualization (not by rendering null in
|
|
162
|
+
// TranscriptRow): a null row never mounts, never enters paintedOnce, and
|
|
163
|
+
// would stall the main-screen history-paint batch loop forever. A row
|
|
164
|
+
// that is STILL STREAMING keeps its place even with empty text — the
|
|
165
|
+
// live dot is the "model is answering" affordance and content may yet
|
|
166
|
+
// arrive.
|
|
167
|
+
let hasEmptyAssistant = false;
|
|
168
|
+
for (const row of sliced) {
|
|
169
|
+
if (row.kind === 'assistant' && row.streaming !== true && (row.text ?? '').trim() === '') {
|
|
170
|
+
hasEmptyAssistant = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const out = hasEmptyAssistant
|
|
175
|
+
? sliced.filter(row => !(row.kind === 'assistant' && row.streaming !== true && (row.text ?? '').trim() === '') &&
|
|
176
|
+
(thinkingVisible || row.kind !== 'reasoning'))
|
|
177
|
+
: thinkingVisible
|
|
178
|
+
? sliced
|
|
179
|
+
: sliced.filter(row => row.kind !== 'reasoning');
|
|
180
|
+
// CC addMargin: every rendered block gets a 1-row top margin except the
|
|
181
|
+
// first. Pre-pass over the FULL list so a windowed row keeps the exact
|
|
182
|
+
// spacing it would have in a fully-mounted list.
|
|
183
|
+
const margins = new Map();
|
|
184
|
+
{
|
|
185
|
+
let prev;
|
|
186
|
+
for (const row of out) {
|
|
187
|
+
margins.set(row.id, prev !== undefined);
|
|
188
|
+
prev = row.kind;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const streamBits = new Uint8Array(rows.length);
|
|
192
|
+
for (let i = 0; i < rows.length; i++)
|
|
193
|
+
streamBits[i] = rows[i].streaming === true ? 1 : 0;
|
|
194
|
+
visibleRowsCacheRef.current = {
|
|
195
|
+
rows,
|
|
196
|
+
rowsLength: rows.length,
|
|
197
|
+
showAll: showAll || hiddenCount <= 0,
|
|
198
|
+
thinkingVisible,
|
|
199
|
+
out,
|
|
200
|
+
margins,
|
|
201
|
+
streamBits,
|
|
202
|
+
};
|
|
203
|
+
visGenRef.current++;
|
|
204
|
+
}
|
|
205
|
+
const visibleRows = visibleRowsCacheRef.current.out;
|
|
206
|
+
const margins = visibleRowsCacheRef.current.margins;
|
|
59
207
|
// Selection keeps its highlight; expanded rows render with no fill (the
|
|
60
208
|
// diff line tints inside cards are the only backgrounds in the transcript).
|
|
61
209
|
const rowBackground = (rowId) => {
|
|
@@ -74,6 +222,9 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
74
222
|
// DEFAULT_ROW_HEIGHT, which only perturbs deep scrollback estimates.
|
|
75
223
|
const HEIGHTS_CACHE_MAX = 5000;
|
|
76
224
|
const heightsRef = React.useRef(new Map());
|
|
225
|
+
/** Geometry version: bumped at EVERY heightsRef mutation so downstream
|
|
226
|
+
* memos (timeline tops) can key on it instead of re-deriving offsets. */
|
|
227
|
+
const heightsVersionRef = React.useRef(0);
|
|
77
228
|
const localRefs = React.useRef(new Map());
|
|
78
229
|
/** Row ids that have been mounted (and therefore painted into the
|
|
79
230
|
* terminal) at least once. The sticky window may skip a row ONLY after
|
|
@@ -94,9 +245,20 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
94
245
|
* free: those rows sit in scrollback and the diff skips them. */
|
|
95
246
|
const lastStartRef = React.useRef(-1);
|
|
96
247
|
const holdUntilRef = React.useRef(0);
|
|
248
|
+
/** True when frame-budgeted history painting still has batches left
|
|
249
|
+
* (main-screen open): the layout effect schedules the next slice. */
|
|
250
|
+
const paintPendingRef = React.useRef(false);
|
|
251
|
+
const paintQueuedRef = React.useRef(false);
|
|
252
|
+
/** Persistent history-paint edge: how far batched painting has advanced
|
|
253
|
+
* (index into visibleRows). -1 = not painting / reset (list head change:
|
|
254
|
+
* rewind, new session, loadOlder — must repaint from scratch). */
|
|
255
|
+
const paintEdgeRef = React.useRef(-1);
|
|
97
256
|
const listHeadId = visibleRows[0]?.id;
|
|
98
257
|
if (listHeadId !== undefined && paintedBaseRef.current !== undefined && listHeadId !== paintedBaseRef.current) {
|
|
99
258
|
paintedOnceRef.current = new Set();
|
|
259
|
+
// History repaints from scratch after a head change too (rewind /
|
|
260
|
+
// loadOlder prepends rows that must paint again).
|
|
261
|
+
paintEdgeRef.current = -1;
|
|
100
262
|
}
|
|
101
263
|
if (listHeadId !== undefined)
|
|
102
264
|
paintedBaseRef.current = listHeadId;
|
|
@@ -110,8 +272,66 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
110
272
|
if (lastColumns.current !== columns) {
|
|
111
273
|
lastColumns.current = columns;
|
|
112
274
|
heightsRef.current.clear();
|
|
275
|
+
heightsVersionRef.current++;
|
|
113
276
|
baseRef.current = null;
|
|
114
277
|
}
|
|
278
|
+
// --- layout signature: stale-height invalidation ------------------------
|
|
279
|
+
// heightsRef entries outlive the commits that measured them, but many
|
|
280
|
+
// state changes rewrite a row's height WITHOUT a columns change: Ctrl+O
|
|
281
|
+
// (expanded), single-row expand (expandedRows), reasoning stream→fold,
|
|
282
|
+
// a tool result/error/footnote arriving, diff layout switch, assistant
|
|
283
|
+
// text growth, thinking visibility. A cached height from before such a
|
|
284
|
+
// change feeds topPad/bottomPad spacers, the offsets scan, and the
|
|
285
|
+
// ScrollBox clamps with geometry that no longer exists — blank bands,
|
|
286
|
+
// overlapping rows, wrong scrollTop after toggles (the audit's stale
|
|
287
|
+
// height cache). Track the inputs that decide each row's height; when
|
|
288
|
+
// one changes, drop the cached height. The window extension further
|
|
289
|
+
// down remounts invalidated rows so useLayoutEffect re-measures them.
|
|
290
|
+
// Text identity uses length as an O(1) proxy — per-frame full-text
|
|
291
|
+
// hashing over every row would defeat virtualization's budget, and a
|
|
292
|
+
// same-length miss only degrades to the previous behavior.
|
|
293
|
+
//
|
|
294
|
+
// VECTORS, not strings: the cache stores the parts VECTOR and the
|
|
295
|
+
// comparison is slot-by-slot — the unchanged case (every settled row on
|
|
296
|
+
// every scroll tick) allocates nothing. A joined string per row per
|
|
297
|
+
// render was O(rows) garbage per tick (3200-row session ⇒ several MB/s
|
|
298
|
+
// into minor GC; the GC share of the scroll profile).
|
|
299
|
+
const sigRef = React.useRef(new Map());
|
|
300
|
+
{
|
|
301
|
+
const sigs = sigRef.current;
|
|
302
|
+
for (let i = 0; i < visibleRows.length; i++) {
|
|
303
|
+
const row = visibleRows[i];
|
|
304
|
+
// Per-kind signature: only the inputs that row's OWN renderer consumes.
|
|
305
|
+
// A global flat array (every field × every row) over-invalidates — a
|
|
306
|
+
// /settings diffLayout switch used to drop every user/assistant/
|
|
307
|
+
// reasoning height at once, remounting the widened window over rows
|
|
308
|
+
// whose rendering never changed (Yoga spike + measure churn for
|
|
309
|
+
// nothing). Base parts cover the universal height inputs.
|
|
310
|
+
const parts = signatureParts(row, columns, expanded, expandedRows, streamFoldedRows, thinkingVisible, thinkingFold, diffLayout, model, failureHintRowId, failureHint);
|
|
311
|
+
const cachedParts = sigs.get(row.id);
|
|
312
|
+
let same = false;
|
|
313
|
+
if (cachedParts !== undefined && cachedParts.length === parts.length) {
|
|
314
|
+
same = true;
|
|
315
|
+
for (let s = 0; s < parts.length; s++) {
|
|
316
|
+
if (parts[s] !== cachedParts[s]) {
|
|
317
|
+
same = false;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (same)
|
|
323
|
+
continue;
|
|
324
|
+
if (sigs.size >= HEIGHTS_CACHE_MAX) {
|
|
325
|
+
const oldest = sigs.keys().next().value;
|
|
326
|
+
if (oldest !== undefined)
|
|
327
|
+
sigs.delete(oldest);
|
|
328
|
+
}
|
|
329
|
+
// Copy out of the module-level scratch buffer — the next row reuses it.
|
|
330
|
+
sigs.set(row.id, parts.slice());
|
|
331
|
+
heightsRef.current.delete(row.id);
|
|
332
|
+
heightsVersionRef.current++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
115
335
|
// Scrolling bypasses React (imperative DOM scrollTop): subscribe so the
|
|
116
336
|
// window follows the viewport.
|
|
117
337
|
React.useEffect(() => {
|
|
@@ -120,8 +340,17 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
120
340
|
const tick = () => { setScrollTick(t => t + 1); };
|
|
121
341
|
return scrollHandle.subscribe(tick);
|
|
122
342
|
}, [scrollHandle]);
|
|
343
|
+
// Cached rail/header preview per user row (see the timeline block for why
|
|
344
|
+
// the length guard exists alongside the id key).
|
|
345
|
+
const previewCacheRef = React.useRef(new Map());
|
|
123
346
|
const heightOf = (row) => heightsRef.current.get(row.id) ?? DEFAULT_ROW_HEIGHT;
|
|
124
|
-
|
|
347
|
+
// Reused offsets buffer: the prefix-scan itself must run every render
|
|
348
|
+
// (heightsRef mutates in the measure effect), but the ARRAY need not be
|
|
349
|
+
// fresh — offsets never escapes this render scope. A new 3200-slot array
|
|
350
|
+
// per scroll tick was pure GC fodder.
|
|
351
|
+
const offsetsBufRef = React.useRef([]);
|
|
352
|
+
const offsets = offsetsBufRef.current;
|
|
353
|
+
offsets.length = visibleRows.length;
|
|
125
354
|
let total = 0;
|
|
126
355
|
for (let i = 0; i < visibleRows.length; i++) {
|
|
127
356
|
offsets[i] = total;
|
|
@@ -188,9 +417,87 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
188
417
|
// vanish from the user's scrollback. Extending mounts everything above
|
|
189
418
|
// on the first frame (topPad 0, full paint), then the set fills and the
|
|
190
419
|
// window tightens to the tail.
|
|
420
|
+
// MAIN-SCREEN ONLY (historyPaintEnabled): the alt-screen has no
|
|
421
|
+
// scrollback — a row outside the window has no "copy" to preserve, and
|
|
422
|
+
// mounting the whole fold window on open lexed/highlighted/laid out
|
|
423
|
+
// hundreds of markdown rows the user never sees (measured: 4.3s of
|
|
424
|
+
// saturated main thread before first paint on a 960-row session; the
|
|
425
|
+
// virtualization window re-mounts rows on demand as they scroll in).
|
|
426
|
+
// FRAME-BUDGETED: mounting the whole window in ONE commit saturated the
|
|
427
|
+
// main thread for seconds (measured 6s wall with ZERO frames in the
|
|
428
|
+
// first second on an 800-row inline open — every input queued behind
|
|
429
|
+
// the React render). Extend in batches of ~2 viewports of measured
|
|
430
|
+
// height per commit instead; the pending-batch effect schedules the
|
|
431
|
+
// next slice, so the app paints, drains input, and stays interactive
|
|
432
|
+
// while history streams in above the fold (opencode-style progressive
|
|
433
|
+
// transcript hydration; the terminal accepts rows whenever they land).
|
|
191
434
|
const paintedOnce = paintedOnceRef.current;
|
|
435
|
+
let paintPending = false;
|
|
436
|
+
if (historyPaintEnabled) {
|
|
437
|
+
// Persistent batch edge: the sticky floor-walk RESETS start to the
|
|
438
|
+
// tail every frame, so a per-frame budget walk from `start` re-spends
|
|
439
|
+
// its whole budget on already-painted rows and never reaches deeper
|
|
440
|
+
// unpainted ones (measured: an infinite 0.3ms/frame batch loop).
|
|
441
|
+
// Remember how far painting has advanced; each batch extends THAT
|
|
442
|
+
// edge upward by the budget.
|
|
443
|
+
if (paintEdgeRef.current < 0)
|
|
444
|
+
paintEdgeRef.current = start;
|
|
445
|
+
let firstUnpainted = -1;
|
|
446
|
+
for (let i = 0; i < paintEdgeRef.current; i++) {
|
|
447
|
+
if (!paintedOnce.has(visibleRows[i].id)) {
|
|
448
|
+
firstUnpainted = i;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (firstUnpainted !== -1) {
|
|
453
|
+
// Budget: extend the paint edge upward by ~half a viewport of
|
|
454
|
+
// measured content per commit (height estimates; unknown rows fall
|
|
455
|
+
// back to DEFAULT_ROW_HEIGHT and over-mount slightly — harmless).
|
|
456
|
+
// Measured: larger batches (2 viewports) put 50ms+ of first-wrap
|
|
457
|
+
// yoga into one frame; half a viewport keeps batches near the
|
|
458
|
+
// 16ms frame budget while still finishing an 800-row open in
|
|
459
|
+
// well under two seconds of background batches.
|
|
460
|
+
let budget = Math.min(viewport, termRows) / 2 + OVERSCAN_LINES;
|
|
461
|
+
let j = paintEdgeRef.current;
|
|
462
|
+
while (j > firstUnpainted && budget > 0) {
|
|
463
|
+
j--;
|
|
464
|
+
budget -= heightOf(visibleRows[j]);
|
|
465
|
+
}
|
|
466
|
+
paintEdgeRef.current = j;
|
|
467
|
+
start = Math.min(start, j);
|
|
468
|
+
paintPending = j > 0;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
paintEdgeRef.current = -1;
|
|
473
|
+
}
|
|
474
|
+
paintPendingRef.current = paintPending;
|
|
475
|
+
// Unknown-height extension (layout signature, see sigRef): a row whose
|
|
476
|
+
// cached height was just INVALIDATED must remount to re-measure even
|
|
477
|
+
// when it sits outside the window — its spacer otherwise falls back to
|
|
478
|
+
// DEFAULT_ROW_HEIGHT until the row scrolls back into view, leaving the
|
|
479
|
+
// content geometry wrong for exactly that long (blank band after
|
|
480
|
+
// Ctrl+O, unreachable scroll bottom after a tool result lands). One
|
|
481
|
+
// remount per change; the measure tick + hold then tighten again.
|
|
482
|
+
// Guard 1: only rows that have actually MOUNTED here once qualify
|
|
483
|
+
// (paintedOnce fills from localRefs post-commit) — a brand-new
|
|
484
|
+
// streaming row has never been measured, and extending over it would
|
|
485
|
+
// mount everything below the window every frame while the user reads
|
|
486
|
+
// scrolled-up (virtualization defeated, per-frame full mount = the
|
|
487
|
+
// long-session stall). New rows keep the original path: their height
|
|
488
|
+
// lands once the window reaches them.
|
|
489
|
+
// Guard 2: rows actively STREAMING are skipped too. A streaming row's
|
|
490
|
+
// signature invalidates EVERY chunk (text length grows), so a painted
|
|
491
|
+
// streaming row below/above the window remounted per chunk — full
|
|
492
|
+
// markdown re-lex + wrap of the whole growing text, invisible to the
|
|
493
|
+
// user reading history (measured: 3s of streaming while scrolled up
|
|
494
|
+
// burned 2.5s of yoga and +84MB heap). Its height is in flux anyway;
|
|
495
|
+
// the final settle invalidates once more and remounts exactly once.
|
|
192
496
|
for (let i = 0; i < start; i++) {
|
|
193
|
-
if (
|
|
497
|
+
if (visibleRows[i].streaming === true)
|
|
498
|
+
continue;
|
|
499
|
+
const rowId = visibleRows[i].id;
|
|
500
|
+
if (!heightsRef.current.has(rowId) && paintedOnceRef.current.has(rowId)) {
|
|
194
501
|
start = i;
|
|
195
502
|
break;
|
|
196
503
|
}
|
|
@@ -208,6 +515,21 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
208
515
|
}
|
|
209
516
|
lastStartRef.current = start;
|
|
210
517
|
}
|
|
518
|
+
// Tail-side invalidated-height extension (see the start-side loop above
|
|
519
|
+
// for the rationale and both guards): rows BELOW the window whose height
|
|
520
|
+
// was just invalidated remount to re-measure, so bottomPad keeps real
|
|
521
|
+
// geometry while the user reads scrolled-up content and the tail streams.
|
|
522
|
+
// Runs for non-sticky views; sticky mounts the tail anyway. Streaming
|
|
523
|
+
// rows are skipped — THIS loop is the measured hot path of the
|
|
524
|
+
// read-while-streaming stall (the streaming tail row sits below the
|
|
525
|
+
// window and invalidated per chunk).
|
|
526
|
+
for (let i = end; i < visibleRows.length; i++) {
|
|
527
|
+
if (visibleRows[i].streaming === true)
|
|
528
|
+
continue;
|
|
529
|
+
const rowId = visibleRows[i].id;
|
|
530
|
+
if (!heightsRef.current.has(rowId) && paintedOnceRef.current.has(rowId))
|
|
531
|
+
end = i + 1;
|
|
532
|
+
}
|
|
211
533
|
if (forceMountRowId !== undefined && forceMountRowId !== null) {
|
|
212
534
|
const idx = visibleRows.findIndex(row => row.id === forceMountRowId);
|
|
213
535
|
if (idx !== -1) {
|
|
@@ -227,6 +549,20 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
227
549
|
const topPad = offsets[start] ?? 0;
|
|
228
550
|
const mountedBottom = end < visibleRows.length ? offsets[end] : total;
|
|
229
551
|
const bottomPad = total - mountedBottom;
|
|
552
|
+
noteListGeometry({
|
|
553
|
+
start,
|
|
554
|
+
end,
|
|
555
|
+
topPad,
|
|
556
|
+
bottomPad,
|
|
557
|
+
total,
|
|
558
|
+
base,
|
|
559
|
+
sticky,
|
|
560
|
+
pending,
|
|
561
|
+
viewport,
|
|
562
|
+
termRows,
|
|
563
|
+
columns,
|
|
564
|
+
rowCount: visibleRows.length,
|
|
565
|
+
});
|
|
230
566
|
// New-messages pill count: rows past the seen-anchor whose top edge is
|
|
231
567
|
// still below the viewport bottom. Same rows-space math as the window
|
|
232
568
|
// (offsets are rows-space, scrollTop content-space — subtract the header
|
|
@@ -252,6 +588,131 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
252
588
|
onUnseenCount?.(unseenCount);
|
|
253
589
|
}
|
|
254
590
|
});
|
|
591
|
+
// Conversation timeline snapshot: one entry per user turn (stable id +
|
|
592
|
+
// content-space text top + preview), plus the viewport-derived targets
|
|
593
|
+
// consumed by BOTH the sticky prompt header (active) and the transcript
|
|
594
|
+
// turn rail (active highlight, ▲/▼). Top-anchored semantics (Grok
|
|
595
|
+
// timeline): active = the LAST turn whose prompt top is at-or-above the
|
|
596
|
+
// viewport top — the turn whose content owns the top row, "the turn
|
|
597
|
+
// being read" — with the first turn standing in while pre-turn content
|
|
598
|
+
// (logo / loaded-context) owns the top. The highlight moves only when a
|
|
599
|
+
// turn boundary crosses the viewport top, never when a later prompt
|
|
600
|
+
// merely becomes visible lower on screen, so it cannot leap when
|
|
601
|
+
// nudging off the bottom and the header/rail can never disagree. The
|
|
602
|
+
// projected top (scrollTop + pending) is used so boundary crossings
|
|
603
|
+
// register during wheel bursts, not one drain frame late. Same
|
|
604
|
+
// rows-space math as the mount window (offsets are rows-space,
|
|
605
|
+
// scrollTop content-space — add the header base). downId additionally
|
|
606
|
+
// requires the turn's top ≤ maxScroll: the renderer clamps scrollTop
|
|
607
|
+
// there, so a turn past it could never own the top row, and naming it
|
|
608
|
+
// would make ▼ repeat itself forever (the stuck-▼ bug). Reported
|
|
609
|
+
// post-commit, only when the signature changes.
|
|
610
|
+
let timelineTurns = [];
|
|
611
|
+
let activeTurnIndex = null;
|
|
612
|
+
let upTurnIndex = null;
|
|
613
|
+
let downTurnIndex = null;
|
|
614
|
+
const timelineMemoRef = React.useRef(null);
|
|
615
|
+
{
|
|
616
|
+
// Split into (a) a GEOMETRY-memoized turns list and (b) a per-frame
|
|
617
|
+
// allocation-free target scan. Before the split this block rebuilt on
|
|
618
|
+
// EVERY scroll tick: an 800-object turns array, an 800-entry
|
|
619
|
+
// measuredTops Map, and the report effect's turns.map().join('|')
|
|
620
|
+
// signature — several hundred KB of churn per second of scrolling on a
|
|
621
|
+
// tool-heavy session.
|
|
622
|
+
//
|
|
623
|
+
// (a) turns/tops/folded change ONLY when geometry changes: row heights
|
|
624
|
+
// (heightsVersion — bumped at every heightsRef mutation), the visible
|
|
625
|
+
// window's content (visGen — bumped when the visibleRows cache
|
|
626
|
+
// rebuilds), the measured header base, or the rows array growing. Key
|
|
627
|
+
// on those; previews stay in their own id-keyed cache.
|
|
628
|
+
const memo = timelineMemoRef.current;
|
|
629
|
+
const memoKey = `${visGenRef.current}:${heightsVersionRef.current}:${base}:${rows.length}:${columns}`;
|
|
630
|
+
if (memo === null || memo.key !== memoKey) {
|
|
631
|
+
const previewCache = previewCacheRef.current;
|
|
632
|
+
if (previewCache.size > 2000)
|
|
633
|
+
previewCache.clear();
|
|
634
|
+
// Measured tops for turns INSIDE the fold window (user rows are never
|
|
635
|
+
// filtered by the thinking toggle, so a user row absent from
|
|
636
|
+
// visibleRows is exactly a folded one).
|
|
637
|
+
const measuredTops = new Map();
|
|
638
|
+
for (let i = 0; i < visibleRows.length; i++) {
|
|
639
|
+
const row = visibleRows[i];
|
|
640
|
+
if (row.kind !== 'user')
|
|
641
|
+
continue;
|
|
642
|
+
measuredTops.set(row.id, base + offsets[i] + (margins.get(row.id) === true ? 1 : 0));
|
|
643
|
+
}
|
|
644
|
+
// Walk ALL rows (not the fold window): the rail must cover the whole
|
|
645
|
+
// conversation — a tool-heavy session packs 300 rows into a handful
|
|
646
|
+
// of turns, and window-only turns made the rail show "2-3 nodes".
|
|
647
|
+
// Folded turns carry folded:true + top:-1; their tops are unknown
|
|
648
|
+
// until revealed (they are all ABOVE the viewport, so active/down
|
|
649
|
+
// over measured turns is unaffected; ▲ may name a folded turn — its
|
|
650
|
+
// click goes through the reveal path, not scrollTo).
|
|
651
|
+
const turns = [];
|
|
652
|
+
for (const row of rows) {
|
|
653
|
+
if (row.kind !== 'user')
|
|
654
|
+
continue;
|
|
655
|
+
let cached = previewCache.get(row.id);
|
|
656
|
+
if (cached === undefined || cached.len !== row.text.length) {
|
|
657
|
+
cached = { len: row.text.length, preview: clipPreview(row.text) };
|
|
658
|
+
previewCache.set(row.id, cached);
|
|
659
|
+
}
|
|
660
|
+
const textTop = measuredTops.get(row.id);
|
|
661
|
+
if (textTop !== undefined) {
|
|
662
|
+
turns.push({ id: row.id, top: textTop, preview: cached.preview });
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
turns.push({ id: row.id, top: -1, preview: cached.preview, folded: true });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
timelineMemoRef.current = { key: memoKey, turns };
|
|
669
|
+
}
|
|
670
|
+
timelineTurns = timelineMemoRef.current.turns;
|
|
671
|
+
// (b) per-frame target scan — pure integer comparisons over the
|
|
672
|
+
// memoized list; viewTop is projected (scrollTop + pending) so
|
|
673
|
+
// boundary crossings register during wheel bursts, not one drain
|
|
674
|
+
// frame late. downId additionally requires top ≤ maxScroll: the
|
|
675
|
+
// renderer clamps scrollTop there, so a turn past it could never own
|
|
676
|
+
// the top row, and naming it would make ▼ repeat itself forever.
|
|
677
|
+
const viewTop = scrollTop + pending;
|
|
678
|
+
const maxScroll = Math.max(0, (scrollHandle?.getScrollHeight() ?? 0) - viewport);
|
|
679
|
+
for (let i = 0; i < timelineTurns.length; i++) {
|
|
680
|
+
const t = timelineTurns[i];
|
|
681
|
+
if (t.folded === true) {
|
|
682
|
+
// Above the fold ⇒ strictly above the viewport: a legal ▲ target.
|
|
683
|
+
upTurnIndex = i;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (t.top <= viewTop)
|
|
687
|
+
activeTurnIndex = i;
|
|
688
|
+
if (t.top < viewTop)
|
|
689
|
+
upTurnIndex = i;
|
|
690
|
+
if (downTurnIndex === null && t.top > viewTop && t.top <= maxScroll) {
|
|
691
|
+
downTurnIndex = i;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (timelineTurns.length > 0 && activeTurnIndex === null)
|
|
695
|
+
activeTurnIndex = 0;
|
|
696
|
+
}
|
|
697
|
+
const timeline = {
|
|
698
|
+
turns: timelineTurns,
|
|
699
|
+
activeId: activeTurnIndex === null ? null : timelineTurns[activeTurnIndex].id,
|
|
700
|
+
upId: upTurnIndex === null ? null : timelineTurns[upTurnIndex].id,
|
|
701
|
+
downId: downTurnIndex === null ? null : timelineTurns[downTurnIndex].id,
|
|
702
|
+
};
|
|
703
|
+
const lastTimelineReportRef = React.useRef('');
|
|
704
|
+
React.useEffect(() => {
|
|
705
|
+
// O(1) signature: the memo key pins the turns' geometry identity
|
|
706
|
+
// (heights/window/base/rows-length) and the three ids pin the
|
|
707
|
+
// viewport-derived targets. Any top change bumps the geometry key, so
|
|
708
|
+
// the report still fires exactly when the snapshot content changes —
|
|
709
|
+
// without rebuilding an O(turns) joined string per commit.
|
|
710
|
+
const sig = `${timelineMemoRef.current?.key ?? ''}#a${timeline.activeId}#u${timeline.upId}#d${timeline.downId}`;
|
|
711
|
+
if (sig !== lastTimelineReportRef.current) {
|
|
712
|
+
lastTimelineReportRef.current = sig;
|
|
713
|
+
onTimeline?.(timeline);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
255
716
|
// Post-commit: measure mounted rows, derive the content-space base from
|
|
256
717
|
// the first mounted row's Yoga top, and clamp render-time scrollTop to the
|
|
257
718
|
// mounted coverage so burst scrolls never show blank spacer.
|
|
@@ -272,6 +733,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
272
733
|
heightsRef.current.delete(oldest);
|
|
273
734
|
}
|
|
274
735
|
heightsRef.current.set(id, h);
|
|
736
|
+
heightsVersionRef.current++;
|
|
275
737
|
changed = true;
|
|
276
738
|
}
|
|
277
739
|
}
|
|
@@ -301,7 +763,17 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
301
763
|
}
|
|
302
764
|
else {
|
|
303
765
|
const min = Math.max(0, base + topPad - viewport);
|
|
304
|
-
|
|
766
|
+
// Upper clamp only while unmounted content remains below the window
|
|
767
|
+
// (bottomPad spacer): it exists to pin the paint to the mounted edge
|
|
768
|
+
// during burst scrolls that outrun React's window re-render. Once the
|
|
769
|
+
// tail is fully mounted (bottomPad 0) there IS no unmounted gap — the
|
|
770
|
+
// estimated `mountedBottom` can sit a line short of the real Yoga
|
|
771
|
+
// extent (engine flex-basis cache vs final child layout drift, see
|
|
772
|
+
// render-node-to-output's scrollHeight floor), and an estimated clamp
|
|
773
|
+
// would then cull the last line at every non-sticky paint — the
|
|
774
|
+
// scrolled-away-and-back tail loss. Leave the max open; the renderer
|
|
775
|
+
// still caps at the frame's real maxScroll.
|
|
776
|
+
scrollHandle.setClampBounds(min, bottomPad <= 0 ? undefined : Math.max(min, base + mountedBottom - viewport));
|
|
305
777
|
}
|
|
306
778
|
}
|
|
307
779
|
if (changed && !measureQueuedRef.current) {
|
|
@@ -310,9 +782,27 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
310
782
|
measureQueuedRef.current = true;
|
|
311
783
|
queueMicrotask(() => {
|
|
312
784
|
measureQueuedRef.current = false;
|
|
785
|
+
noteFrameCause('measure');
|
|
313
786
|
setMeasureTick(t => t + 1);
|
|
314
787
|
});
|
|
315
788
|
}
|
|
789
|
+
// History-paint continuation (main-screen open): more never-painted
|
|
790
|
+
// batches remain — schedule the next slice on the macrotask queue so
|
|
791
|
+
// pending input events (wheel, keys) drain between batches and the app
|
|
792
|
+
// stays interactive while preset history streams in. A timeout of 0 is
|
|
793
|
+
// enough: each batch's mount+measure work is bounded (~2 viewports),
|
|
794
|
+
// unlike the previous single-commit full mount that saturated the main
|
|
795
|
+
// thread for seconds.
|
|
796
|
+
if (paintPendingRef.current && !paintQueuedRef.current) {
|
|
797
|
+
paintQueuedRef.current = true;
|
|
798
|
+
setTimeout(() => {
|
|
799
|
+
paintQueuedRef.current = false;
|
|
800
|
+
if (!paintPendingRef.current)
|
|
801
|
+
return;
|
|
802
|
+
noteFrameCause('measure');
|
|
803
|
+
setMeasureTick(t => t + 1);
|
|
804
|
+
}, 0);
|
|
805
|
+
}
|
|
316
806
|
});
|
|
317
807
|
// useCallback: the reference feeds MemoRow's shallow compare; a fresh
|
|
318
808
|
// closure per render would defeat every row's memo.
|
|
@@ -323,11 +813,7 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
323
813
|
localRefs.current.delete(rowId);
|
|
324
814
|
registerRowRef?.(rowId, el);
|
|
325
815
|
}, [registerRowRef]);
|
|
326
|
-
|
|
327
|
-
// Computed per render (cheap) but only forwarded to running rows, so
|
|
328
|
-
// settled rows never see a changing prop.
|
|
329
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
330
|
-
return (_jsxs(_Fragment, { children: [rows.some(row => row.folded) && (_jsx(Box, { marginTop: 1, onClick: onLoadOlder, children: _jsx(Divider, { title: t('load-earlier') }) })), !showAll && hiddenCount > 0 && (_jsx(Box, { marginTop: 1, onClick: onToggleAll, children: _jsx(Divider, { title: t('show-previous-messages', { n: hiddenCount }) }) })), topPad > 0 && _jsx(Box, { height: topPad, flexShrink: 0 }), visibleRows
|
|
816
|
+
return (_jsxs(_Fragment, { children: [rows.some(row => row.folded) && (_jsx(ClickableDivider, { title: t('load-earlier'), onClick: onLoadOlder })), !showAll && hiddenCount > 0 && (_jsx(ClickableDivider, { title: t('show-previous-messages', { n: hiddenCount }), onClick: onToggleAll })), topPad > 0 && _jsx(Box, { height: topPad, flexShrink: 0 }), visibleRows
|
|
331
817
|
.slice(start, end)
|
|
332
818
|
.map((row) => {
|
|
333
819
|
// CC addMargin: pre-pass result keeps windowed rows at full-mount
|
|
@@ -335,30 +821,56 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
335
821
|
const addMargin = margins.get(row.id) === true;
|
|
336
822
|
const tool = row.tool;
|
|
337
823
|
const subagent = row.kind === 'subagent' ? row.subagent : undefined;
|
|
338
|
-
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, 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,
|
|
824
|
+
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, 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, onToggleStreamFold: onToggleStreamFold, streamFolded: streamFoldedRows.has(row.id), onOpenSubagent: onOpenSubagent, setRowRef: setRowRef }, row.id));
|
|
339
825
|
}), bottomPad > 0 && _jsx(Box, { height: bottomPad, flexShrink: 0 })] }));
|
|
340
826
|
}
|
|
341
|
-
|
|
827
|
+
/** Load-earlier / show-previous divider row with a mouse hover tint — the
|
|
828
|
+
* clickability is otherwise invisible (audit C-04). */
|
|
829
|
+
function ClickableDivider({ title, onClick }) {
|
|
830
|
+
const [hovered, setHovered] = useState(false);
|
|
831
|
+
return (_jsx(Box, { marginTop: 1, onClick: onClick, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), backgroundColor: hovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Divider, { title: title }) }));
|
|
832
|
+
}
|
|
833
|
+
function TranscriptRow({ rowId, kind, text, executionTarget, streaming, durationMs, time, addMargin, isSelected, isExpanded, expanded, model, diffLayout, thinkingFold, toolBackground, activityFrames, background, toolCallId, toolName, toolArgsText, toolArgsFull, toolStatus, toolResultText, toolResultFull, toolErrorText, toolFootnote, toolCallView, toolResultView, toolStartedAt, toolDurationMs, subagent, onToggleRow, onToggleStreamFold, streamFolded, onOpenSubagent, setRowRef, }) {
|
|
342
834
|
const ref = React.useCallback((el) => {
|
|
343
835
|
setRowRef(rowId, el);
|
|
344
836
|
}, [setRowRef, rowId]);
|
|
345
|
-
|
|
837
|
+
// 可折叠行(工具卡/思考/compact 摘要)共用:点击切换展开,全宽行右侧
|
|
838
|
+
// 空白(屏幕缓冲未写入单元格)不触发——点击空白想选字/拖拽时不再误触
|
|
839
|
+
// 展开/收起(审计 C-03/cellIsBlank 零消费)。纯文本行(user/assistant)
|
|
840
|
+
// 保持不可点:转录是阅读区(用户反馈),折叠语义留给带视觉指示的行。
|
|
841
|
+
const foldOnClick = React.useCallback((event) => {
|
|
842
|
+
if (event.cellIsBlank)
|
|
843
|
+
return;
|
|
346
844
|
onToggleRow(rowId);
|
|
347
845
|
}, [onToggleRow, rowId]);
|
|
846
|
+
// 流式 reasoning 行:点击折叠/展开 live 视图(默认展开,与落定行的
|
|
847
|
+
// 默认折叠相反——所以走独立开关,落定后语义自动回到 foldOnClick)。
|
|
848
|
+
const streamFoldOnClick = React.useCallback((event) => {
|
|
849
|
+
if (event.cellIsBlank)
|
|
850
|
+
return;
|
|
851
|
+
onToggleStreamFold(rowId);
|
|
852
|
+
}, [onToggleStreamFold, rowId]);
|
|
853
|
+
// 子代理卡:点击打开详情场景(不是折叠)。
|
|
854
|
+
const openSubagent = React.useCallback(() => {
|
|
855
|
+
if (subagent !== undefined)
|
|
856
|
+
onOpenSubagent?.(subagent.agentId);
|
|
857
|
+
}, [onOpenSubagent, subagent]);
|
|
858
|
+
// compact 摘要折叠行 hover 轻指示(∴ 提亮,不刷背景)。
|
|
859
|
+
const [compactHovered, setCompactHovered] = useState(false);
|
|
348
860
|
switch (kind) {
|
|
349
861
|
case 'user':
|
|
350
|
-
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(UserPromptMessage, { text: text, addMargin: addMargin, isSelected: isSelected
|
|
862
|
+
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(UserPromptMessage, { text: text, addMargin: addMargin, isSelected: isSelected }) }));
|
|
351
863
|
case 'assistant':
|
|
352
|
-
return streaming ? (_jsxs(Box, { alignItems: "flex-start", flexDirection: "row", marginTop: addMargin ? 1 : 0, width: "100%", backgroundColor: background, ref: ref, children: [_jsx(Box, { minWidth: 2, children: _jsx(Text, { color: "text", children: "\u25CF" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(StreamingMarkdown, { children: stripNarration(text) }) })] })) : (_jsxs(Box, { width: "100%", flexDirection: "column", backgroundColor: background, ref: ref, children: [expanded && (_jsx(Box, { flexDirection: "row", justifyContent: "flex-end", gap: 1, marginTop: 1, children: _jsx(MessageMetadata, { timestamp: time, model: model }) })), _jsx(AssistantTextMessage, { text: stripNarration(text), addMargin: addMargin, isSelected: isSelected, isExpanded: isExpanded
|
|
864
|
+
return streaming ? (_jsxs(Box, { alignItems: "flex-start", flexDirection: "row", marginTop: addMargin ? 1 : 0, width: "100%", backgroundColor: background, ref: ref, children: [_jsx(Box, { minWidth: 2, children: _jsx(Text, { color: "text", children: "\u25CF" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(StreamingMarkdown, { children: stripNarration(text) }) })] })) : (_jsxs(Box, { width: "100%", flexDirection: "column", backgroundColor: background, ref: ref, children: [expanded && (_jsx(Box, { flexDirection: "row", justifyContent: "flex-end", gap: 1, marginTop: 1, children: _jsx(MessageMetadata, { timestamp: time, model: model }) })), _jsx(AssistantTextMessage, { text: stripNarration(text), addMargin: addMargin, isSelected: isSelected, isExpanded: isExpanded })] }));
|
|
353
865
|
case 'reasoning':
|
|
354
866
|
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(AssistantThinkingMessage, { thinking: text, addMargin: addMargin, streaming: streaming, preview: streaming &&
|
|
867
|
+
!streamFolded &&
|
|
355
868
|
thinkingFold === 'preview' &&
|
|
356
|
-
!expanded &&
|
|
357
869
|
!isExpanded,
|
|
358
|
-
// Streaming reasoning shows expanded live
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
verbose: isExpanded || expanded || streaming, durationMs: durationMs, isSelected: isSelected, onClick:
|
|
870
|
+
// Streaming reasoning shows expanded live (click collapses to the
|
|
871
|
+
// ticker/header via streamFolded); settled rows keep the
|
|
872
|
+
// fold-on-settle default and expand via expandedRows/Ctrl+O.
|
|
873
|
+
verbose: isExpanded || expanded || (streaming && !streamFolded), durationMs: durationMs, isSelected: isSelected, onClick: streaming ? streamFoldOnClick : foldOnClick }) }));
|
|
362
874
|
case 'tool': {
|
|
363
875
|
if (toolCallId === undefined ||
|
|
364
876
|
toolName === undefined ||
|
|
@@ -383,7 +895,7 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
|
|
|
383
895
|
startedAt: toolStartedAt,
|
|
384
896
|
durationMs: toolDurationMs,
|
|
385
897
|
};
|
|
386
|
-
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 }) }));
|
|
898
|
+
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, onClick: foldOnClick }) }));
|
|
387
899
|
}
|
|
388
900
|
case 'notice':
|
|
389
901
|
return (_jsx(Box, { marginTop: 1, ref: ref, children: _jsx(Divider, { title: ` ${text} ` }) }));
|
|
@@ -396,13 +908,13 @@ function TranscriptRow({ rowId, kind, text, executionTarget, streaming, duration
|
|
|
396
908
|
return (_jsx(Box, { paddingLeft: 2, backgroundColor: background, ref: ref, children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
397
909
|
case 'compact':
|
|
398
910
|
// The post-compaction summary defaults to a folded one-liner with a
|
|
399
|
-
// text preview; Ctrl+O (global)
|
|
400
|
-
// the full summary.
|
|
401
|
-
return (_jsx(Box, { marginTop: addMargin ? 1 : 0, paddingLeft: 2, backgroundColor: background, ref: ref, onClick:
|
|
911
|
+
// text preview; Ctrl+O (global), message-selection Enter, or a click
|
|
912
|
+
// reveals the full summary.
|
|
913
|
+
return (_jsx(Box, { marginTop: addMargin ? 1 : 0, paddingLeft: 2, backgroundColor: background, ref: ref, onClick: foldOnClick, onMouseEnter: () => setCompactHovered(true), onMouseLeave: () => setCompactHovered(false), children: expanded || isExpanded ? (_jsx(Text, { dimColor: true, children: text })) : (_jsxs(Text, { dimColor: true, italic: true, color: compactHovered ? 'text' : undefined, children: [_jsx(Text, { color: compactHovered ? 'text' : undefined, children: "\u2234" }), ' ', t('compact-summary-folded'), " \u00B7 ", compactPreview(text), ' ', t('hint-expand-ctrl-o')] })) }));
|
|
402
914
|
case 'subagent':
|
|
403
915
|
if (!subagent)
|
|
404
916
|
return null;
|
|
405
|
-
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick:
|
|
917
|
+
return (_jsx(Box, { flexDirection: "column", ref: ref, children: _jsx(SubagentMessage, { subagent: subagent, addMargin: addMargin, activityFrames: activityFrames, isExpanded: isExpanded, onClick: openSubagent }) }));
|
|
406
918
|
}
|
|
407
919
|
}
|
|
408
920
|
/** Folded compact-summary preview: whitespace flattened, capped with an
|
|
@@ -419,10 +931,10 @@ const MemoRow = React.memo(TranscriptRow);
|
|
|
419
931
|
* welcome line. It scrolls away with the transcript once the conversation
|
|
420
932
|
* fills the viewport (Claude Code shows its ✦ logo in the same slot).
|
|
421
933
|
*/
|
|
422
|
-
export function LogoHeader({ model, effort, cwd, whale = true, }) {
|
|
934
|
+
export function LogoHeader({ model, effort, cwd, whale = true, skipIntro = false, }) {
|
|
423
935
|
// Minimal mode drops the whole splash (whale art AND wordmark) — only the
|
|
424
936
|
// transcript and a bare status bar remain.
|
|
425
937
|
if (isMinimalMode())
|
|
426
938
|
return null;
|
|
427
|
-
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(LogoV2, { model: model, effort: effort, cwd: cwd, whale: whale }) }));
|
|
939
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(LogoV2, { model: model, effort: effort, cwd: cwd, whale: whale, skipIntro: skipIntro }) }));
|
|
428
940
|
}
|