ubume 0.1.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.
Files changed (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,302 @@
1
+ import React, { memo, useEffect, useMemo, useRef } from "react";
2
+ import { Box, Static, Text } from "ink";
3
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
4
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
5
+ import * as renderDebug from "../../core/perf/renderDebug.js";
6
+ import type { TimelineEvent, UIState } from "../../session/types.js";
7
+ import {
8
+ buildActiveRenderItems,
9
+ buildIntroRenderItem,
10
+ buildStaticRenderItems,
11
+ buildTimelineItems,
12
+ TimelineRowView,
13
+ } from "./Timeline.js";
14
+ import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
15
+ import { LIVE_WINDOW_SAFETY_ROWS, windowLiveRows } from "./liveViewportWindow.js";
16
+ import { buildStaticTranscript, createStaticTranscriptCache, type StaticTranscriptCache } from "./staticTranscriptCache.js";
17
+ import { getShellHeight, getShellWidth, resolveStartupHeaderMode, type TerminalViewport } from "../layout.js";
18
+ import { LOGO_COMPACT, LOGO_COMPACT_MIN_COLS, LOGO_LARGE, LOGO_MEDIUM, selectLogoVariant } from "../render/logoVariants.js";
19
+ import { useTheme } from "../theme.js";
20
+
21
+ export interface TranscriptShellProps {
22
+ layout: TerminalViewport;
23
+ authState: CodexAuthState;
24
+ workspaceLabel: string;
25
+ workspaceRoot?: string | null;
26
+ runtimeSummary?: RuntimeSummary | null;
27
+ staticEvents: TimelineEvent[];
28
+ activeEvents: TimelineEvent[];
29
+ uiState: UIState;
30
+ composer: React.ReactNode;
31
+ composerRows?: number;
32
+ notice?: string | null;
33
+ verboseMode?: boolean;
34
+ clearCount?: number;
35
+ repaintGeneration?: number;
36
+ visible?: boolean;
37
+ }
38
+
39
+ function isTranscriptEvent(event: TimelineEvent): boolean {
40
+ return event.type === "user" || event.type === "assistant" || event.type === "run" || event.type === "shell";
41
+ }
42
+
43
+ export function isHomeScreenState({
44
+ staticEvents,
45
+ activeEvents,
46
+ uiState,
47
+ }: {
48
+ staticEvents: TimelineEvent[];
49
+ activeEvents: TimelineEvent[];
50
+ uiState: UIState;
51
+ }): boolean {
52
+ return uiState.kind === "IDLE"
53
+ && !staticEvents.some(isTranscriptEvent)
54
+ && !activeEvents.some(isTranscriptEvent);
55
+ }
56
+
57
+ function getLogoVariantName(rows: readonly string[]): "large" | "medium" | "compact" | "wordmark" | "none" {
58
+ if (rows.length === 0) return process.env["UBUME_NO_ASCII_LOGO"] === "1" ? "none" : "wordmark";
59
+ if (rows === LOGO_LARGE || rows.join("\n") === LOGO_LARGE.join("\n")) return "large";
60
+ if (rows === LOGO_MEDIUM || rows.join("\n") === LOGO_MEDIUM.join("\n")) return "medium";
61
+ if (rows === LOGO_COMPACT || rows.join("\n") === LOGO_COMPACT.join("\n")) return "compact";
62
+ return "wordmark";
63
+ }
64
+
65
+ function getLogoHiddenReason({
66
+ startupHeaderMode,
67
+ logoVariant,
68
+ width,
69
+ }: {
70
+ startupHeaderMode: ReturnType<typeof resolveStartupHeaderMode>;
71
+ logoVariant: ReturnType<typeof getLogoVariantName>;
72
+ width: number;
73
+ }): string | null {
74
+ if (startupHeaderMode === "tiny") return "terminal-too-small";
75
+ if (process.env["UBUME_NO_ASCII_LOGO"] === "1") return "UBUME_NO_ASCII_LOGO";
76
+ if (logoVariant === "wordmark") return `no-ascii-variant-fits-width-${width}`;
77
+ if (logoVariant === "none") return `no-logo-variant-fits-width-${width}`;
78
+ return null;
79
+ }
80
+
81
+ function TranscriptShellInner({
82
+ layout,
83
+ authState,
84
+ workspaceLabel,
85
+ workspaceRoot = null,
86
+ runtimeSummary = null,
87
+ staticEvents,
88
+ activeEvents,
89
+ uiState,
90
+ composer,
91
+ composerRows,
92
+ notice = null,
93
+ verboseMode = false,
94
+ clearCount = 0,
95
+ visible = true,
96
+ }: TranscriptShellProps) {
97
+ const theme = useTheme();
98
+ const startupHeaderMode = useMemo(
99
+ () => resolveStartupHeaderMode({
100
+ cols: layout.cols,
101
+ rows: layout.rows,
102
+ introRows: 8,
103
+ composerRows: composerRows ?? 5,
104
+ }),
105
+ [composerRows, layout.cols, layout.rows],
106
+ );
107
+ const homeScreenActive = visible && isHomeScreenState({ staticEvents, activeEvents, uiState });
108
+ const shellWidth = getShellWidth(layout.cols);
109
+ const introInnerWidth = Math.max(10, shellWidth - 2);
110
+ const selectedLogoRows = startupHeaderMode === "tiny"
111
+ ? []
112
+ : startupHeaderMode === "large"
113
+ ? selectLogoVariant(introInnerWidth)
114
+ : introInnerWidth >= LOGO_COMPACT_MIN_COLS ? LOGO_COMPACT : [];
115
+ const selectedLogoVariant = getLogoVariantName(selectedLogoRows);
116
+ const logoHiddenReason = getLogoHiddenReason({
117
+ startupHeaderMode,
118
+ logoVariant: selectedLogoVariant,
119
+ width: introInnerWidth,
120
+ });
121
+ const startupTraceKeyRef = useRef<string | null>(null);
122
+ // Stable snapshot of the transcript inputs; hold the last visible one while
123
+ // an overlay hides the shell. Identity only changes when the reducer
124
+ // replaces one of these arrays, never on a keystroke or a stream flush.
125
+ const liveTranscript = useMemo(
126
+ () => ({ staticEvents, activeEvents, uiState }),
127
+ [staticEvents, activeEvents, uiState],
128
+ );
129
+ const visibleTranscriptRef = useRef(liveTranscript);
130
+ if (visible) {
131
+ visibleTranscriptRef.current = liveTranscript;
132
+ }
133
+ const renderedTranscript = visibleTranscriptRef.current;
134
+ const renderedStaticEvents = renderedTranscript.staticEvents;
135
+ const renderedActiveEvents = renderedTranscript.activeEvents;
136
+ const renderedUiState = renderedTranscript.uiState;
137
+ const conversationViewportRows = Math.max(
138
+ 2,
139
+ getShellHeight(layout.rows) - (composerRows ?? 0) - (notice ? 1 : 0),
140
+ );
141
+
142
+ const staticTimelineItems = useMemo(() => buildTimelineItems(renderedStaticEvents), [renderedStaticEvents]);
143
+ const activeTimelineItems = useMemo(() => buildTimelineItems(renderedActiveEvents), [renderedActiveEvents]);
144
+ const staticTurnIds = useMemo(
145
+ () => staticTimelineItems.flatMap((item) => item.type === "turn" ? [item.turnId] : []),
146
+ [staticTimelineItems],
147
+ );
148
+ const activeTurnIds = useMemo(
149
+ () => activeTimelineItems.flatMap((item) => item.type === "turn" ? [item.turnId] : []),
150
+ [activeTimelineItems],
151
+ );
152
+ const allTurnIds = useMemo(() => [...staticTurnIds, ...activeTurnIds], [staticTurnIds, activeTurnIds]);
153
+ const activeTurnId = activeTurnIds[0] ?? null;
154
+
155
+ // Static half: intro + finalized turns, built incrementally. The cache ref
156
+ // lives in this keyed instance, so /clear and width repaints (which remount
157
+ // the shell) start from an empty cache.
158
+ const staticCacheRef = useRef<StaticTranscriptCache | null>(null);
159
+ const staticTranscript = useMemo(() => {
160
+ const cache = staticCacheRef.current ?? (staticCacheRef.current = createStaticTranscriptCache());
161
+ return buildStaticTranscript(
162
+ cache,
163
+ [
164
+ buildIntroRenderItem({
165
+ authState,
166
+ workspaceLabel,
167
+ layout,
168
+ providerLabel: runtimeSummary?.providerLabel ?? null,
169
+ startupHeaderMode,
170
+ }),
171
+ ...buildStaticRenderItems(staticTimelineItems, allTurnIds, activeTurnId, null, null),
172
+ ],
173
+ { totalWidth: shellWidth, verboseMode, workspaceRoot },
174
+ );
175
+ }, [activeTurnId, allTurnIds, authState, layout, runtimeSummary?.providerLabel, shellWidth, startupHeaderMode, staticTimelineItems, verboseMode, workspaceLabel, workspaceRoot]);
176
+
177
+ // Live half: only the running turn, rebuilt per streaming flush.
178
+ const activeTranscript = useMemo(
179
+ () => buildNativeTranscriptParts(
180
+ buildActiveRenderItems(activeTimelineItems, allTurnIds, renderedUiState),
181
+ {
182
+ totalWidth: shellWidth,
183
+ verboseMode,
184
+ debugLabel: "transcript-shell-native",
185
+ workspaceRoot,
186
+ },
187
+ ),
188
+ [activeTimelineItems, allTurnIds, renderedUiState, shellWidth, verboseMode, workspaceRoot],
189
+ );
190
+
191
+ const nativeTranscript = useMemo(() => ({
192
+ staticItems: [...staticTranscript.staticItems, ...activeTranscript.staticItems],
193
+ liveRows: [...staticTranscript.liveRows, ...activeTranscript.liveRows],
194
+ }), [activeTranscript, staticTranscript]);
195
+ const committedRows = useMemo(
196
+ () => nativeTranscript.staticItems.reduce((total, item) => total + item.rows.length, 0),
197
+ [nativeTranscript.staticItems],
198
+ );
199
+ // The whole running turn is measured live (see appendNativeTurnParts), but
200
+ // only its tail is rendered: Ink clears the terminal *and scrollback* on any
201
+ // frame whose live output exceeds the viewport, which yanks a user who
202
+ // scrolled up back to the bottom on every streaming tick. The full turn is
203
+ // committed to <Static> at finalize, so nothing is lost from scrollback.
204
+ const liveWindowRows = Math.max(1, conversationViewportRows - LIVE_WINDOW_SAFETY_ROWS);
205
+ const visibleLiveRows = useMemo(
206
+ () => windowLiveRows(nativeTranscript.liveRows, liveWindowRows),
207
+ [liveWindowRows, nativeTranscript.liveRows],
208
+ );
209
+ const liveRowsHidden = nativeTranscript.liveRows.length - visibleLiveRows.length;
210
+ const spacerRows = Math.max(0, conversationViewportRows - committedRows - visibleLiveRows.length);
211
+
212
+ useEffect(() => {
213
+ const nextKey = [
214
+ layout.cols,
215
+ layout.rows,
216
+ layout.mode,
217
+ startupHeaderMode,
218
+ homeScreenActive ? "home" : "transcript",
219
+ staticEvents.length,
220
+ activeEvents.length,
221
+ uiState.kind,
222
+ selectedLogoVariant,
223
+ clearCount,
224
+ ].join("|");
225
+ if (startupTraceKeyRef.current === nextKey) return;
226
+ startupTraceKeyRef.current = nextKey;
227
+ renderDebug.traceEvent("startup", "homeRender", {
228
+ cols: layout.cols,
229
+ rows: layout.rows,
230
+ layoutMode: layout.mode,
231
+ activeRoot: "TranscriptShell",
232
+ messageCount: [...staticEvents, ...activeEvents].filter(isTranscriptEvent).length,
233
+ staticEventsLength: staticEvents.length,
234
+ activeEventsLength: activeEvents.length,
235
+ uiStateKind: uiState.kind,
236
+ selectedLayoutMode: startupHeaderMode,
237
+ selectedLogoVariant,
238
+ logoBranchSelected: selectedLogoVariant !== "none" && selectedLogoVariant !== "wordmark",
239
+ logoHiddenReason,
240
+ composerCount: visible ? 1 : 0,
241
+ footerCount: visible ? 1 : 0,
242
+ homeScreenRendererUsed: homeScreenActive,
243
+ staticItemCount: staticEvents.length,
244
+ liveRowCount: activeEvents.length,
245
+ liveRowsHidden,
246
+ clearCount,
247
+ });
248
+ }, [
249
+ activeEvents,
250
+ clearCount,
251
+ homeScreenActive,
252
+ layout.cols,
253
+ layout.mode,
254
+ layout.rows,
255
+ liveRowsHidden,
256
+ logoHiddenReason,
257
+ selectedLogoVariant,
258
+ startupHeaderMode,
259
+ staticEvents,
260
+ uiState.kind,
261
+ visible,
262
+ ]);
263
+
264
+ return (
265
+ <Box flexDirection="column" width="100%" display={visible ? "flex" : "none"}>
266
+ <Static key={`native-static-${clearCount}`} items={nativeTranscript.staticItems}>
267
+ {(item: NativeTranscriptRowItem) => <NativeRowsItem key={item.key} rows={item.rows} />}
268
+ </Static>
269
+ {spacerRows > 0 && <Box height={spacerRows} />}
270
+ <NativeRowsItem rows={visibleLiveRows} />
271
+
272
+ {visible && notice && (
273
+ <Box width="100%" paddingX={1}>
274
+ <Text color={theme.success} wrap="truncate">{notice}</Text>
275
+ </Box>
276
+ )}
277
+ {visible && composer}
278
+ </Box>
279
+ );
280
+ }
281
+
282
+ export const TranscriptShell = memo(function TranscriptShell(props: TranscriptShellProps) {
283
+ // repaintGeneration must fold into the outer remount key (not just <Static>'s
284
+ // own key) — Ink only reliably re-flushes already-printed <Static> content on
285
+ // a genuine fresh mount of the whole subtree, confirmed empirically: keying
286
+ // away only the inner <Static> node did not trigger Ink's isStaticDirty/
287
+ // onImmediateRender escape hatch the same way a full remount does.
288
+ return (
289
+ <TranscriptShellInner
290
+ key={`clear-${props.clearCount ?? 0}-repaint-${props.repaintGeneration ?? 0}`}
291
+ {...props}
292
+ />
293
+ );
294
+ });
295
+
296
+ function NativeRowsItem({ rows }: { rows: TimelineRow[] }) {
297
+ return (
298
+ <Box flexDirection="column">
299
+ {rows.map((row) => <TimelineRowView key={row.key} row={row} />)}
300
+ </Box>
301
+ );
302
+ }