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,439 @@
1
+ import React, { memo } from "react";
2
+ import { Box, Text } from "ink";
3
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
4
+ import { formatUbumeBrandLabel } from "../../core/version/channel.js";
5
+ import { UBUME_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
6
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
7
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
8
+ import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
9
+ import * as renderDebug from "../../core/perf/renderDebug.js";
10
+ import { useTheme } from "../theme.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "../layout.js";
12
+ import { getTextWidth } from "../render/textLayout.js";
13
+ import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
14
+ import {
15
+ LOGO_LARGE,
16
+ LOGO_COMPACT,
17
+ LOGO_COMPACT_MIN_COLS,
18
+ LOGO_LARGE_MIN_COLS,
19
+ LOGO_LARGE_MIN_ROWS,
20
+ getLogoWidth,
21
+ } from "../render/logoVariants.js";
22
+
23
+ // Re-exported for backward compatibility with existing tests.
24
+ export const HEADER_WORDMARK_LINES = LOGO_LARGE;
25
+
26
+ const HEADER_PADDING_COLUMNS = 2;
27
+ const SHELL_GUTTER_COLUMNS = 1;
28
+ // Require 130+ cols for wide side-by-side so the UpdateAvailableCard has room.
29
+ const WIDE_HEADER_MIN_COLUMNS = 130;
30
+ // Require 72+ cols for medium side-by-side canonical-logo layout.
31
+ const MEDIUM_HEADER_MIN_COLUMNS = LOGO_LARGE_MIN_COLS;
32
+ const MIN_SIDE_BY_SIDE_METADATA_WIDTH = 18;
33
+ const STACKED_METADATA_GAP_ROWS = 1;
34
+ // Gap row between the UpdateAvailableCard and the metadata lines.
35
+ const UPDATE_CARD_GAP_ROWS = 1;
36
+ // Recommended terminal size shown in the compact-mode hint when the viewport is
37
+ // too small to render any logo art.
38
+ const RECOMMENDED_FULL_HEADER_HINT = `Resize to ≥${LOGO_LARGE_MIN_COLS}×${LOGO_LARGE_MIN_ROWS} for the full Ubume header`;
39
+
40
+ export type HeaderHeroMode = "wide" | "medium" | "narrow" | "compact";
41
+
42
+ export interface HeaderHeroLayout {
43
+ mode: HeaderHeroMode;
44
+ topMarginRows: number;
45
+ bottomMarginRows: number;
46
+ metadataGapColumns: number;
47
+ metadataGapRows: number;
48
+ logoRows: number;
49
+ metadataRows: number;
50
+ // Extra rows used by the compact-mode recommended-size hint (0 in non-compact
51
+ // modes). Threaded through so measureTopHeaderRows matches what renders.
52
+ compactHintRows: number;
53
+ totalRows: number;
54
+ }
55
+
56
+ type HeaderMetadataLine = { key: string; text: string; color: string; bold: boolean };
57
+
58
+ export interface UpdateAvailableInfo {
59
+ latestVersion: string;
60
+ currentVersion: string;
61
+ updateCommand?: string;
62
+ }
63
+
64
+ interface TopHeaderProps {
65
+ authState: CodexAuthState;
66
+ workspaceLabel: string;
67
+ layout: Layout;
68
+ runtimeSummary?: RuntimeSummary | null;
69
+ headerConfig?: HeaderConfig;
70
+ updateAvailable?: UpdateAvailableInfo | null;
71
+ }
72
+
73
+ function getHeaderContentWidth(cols: number): number {
74
+ return Math.max(1, cols - SHELL_GUTTER_COLUMNS - HEADER_PADDING_COLUMNS);
75
+ }
76
+
77
+ function getMetadataRowCount(headerConfig: HeaderConfig): number {
78
+ return [
79
+ headerConfig.showBrand,
80
+ headerConfig.showAuthStatus,
81
+ headerConfig.showWorkspace,
82
+ headerConfig.showProvider,
83
+ ].filter(Boolean).length;
84
+ }
85
+
86
+ function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
87
+ if (!isDecorativeLayoutMode(layout.mode)) {
88
+ return {
89
+ topMarginRows: 0,
90
+ bottomMarginRows: 0,
91
+ };
92
+ }
93
+
94
+ if (layout.rows <= 24) {
95
+ return { topMarginRows: 0, bottomMarginRows: 0 };
96
+ }
97
+
98
+ return {
99
+ topMarginRows: 1,
100
+ bottomMarginRows: 0,
101
+ };
102
+ }
103
+
104
+ export function selectHeaderLogo(layout: Layout): readonly string[] {
105
+ if (process.env["UBUME_NO_ASCII_LOGO"] === "1") return [];
106
+ const showNormalLogo =
107
+ process.env["UBUME_NO_ASCII_LOGO"] !== "1" && (
108
+ layout.mode === "regular" ||
109
+ layout.mode === "expanded" ||
110
+ (layout.mode === "compact" && layout.cols >= 72)
111
+ );
112
+ if (!showNormalLogo) return [];
113
+ if (process.env["UBUME_COMPACT_LOGO"] === "1") return LOGO_COMPACT;
114
+ return LOGO_LARGE;
115
+ }
116
+
117
+ export function getHeaderHeroLayout(
118
+ layout: Layout,
119
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
120
+ hasUpdate = false,
121
+ ): HeaderHeroLayout {
122
+ const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
123
+ const metadataRows = getMetadataRowCount(headerConfig);
124
+ const contentWidth = getHeaderContentWidth(layout.cols);
125
+
126
+ const showNormalLogo =
127
+ process.env["UBUME_NO_ASCII_LOGO"] !== "1" && (
128
+ layout.mode === "regular" ||
129
+ layout.mode === "expanded" ||
130
+ (layout.mode === "compact" && layout.cols >= 72)
131
+ );
132
+
133
+ const placeMetadataBesideLogo = showNormalLogo && layout.cols >= 95;
134
+
135
+ let mode: HeaderHeroMode;
136
+ if (!showNormalLogo) {
137
+ mode = "compact";
138
+ } else if (placeMetadataBesideLogo) {
139
+ mode = layout.cols >= 130 ? "wide" : "medium";
140
+ } else {
141
+ mode = "narrow";
142
+ }
143
+
144
+ const logo = selectHeaderLogo(layout);
145
+ const logoWidth = logo.length > 0 ? getLogoWidth(logo) : 0;
146
+
147
+ if (mode === "compact") {
148
+ // Compact / micro text-only header.
149
+ return {
150
+ mode: "compact",
151
+ topMarginRows,
152
+ bottomMarginRows,
153
+ metadataGapColumns: 0,
154
+ metadataGapRows: 0,
155
+ logoRows: 1,
156
+ metadataRows: 0,
157
+ compactHintRows: 0,
158
+ totalRows: topMarginRows + 1 + bottomMarginRows,
159
+ };
160
+ }
161
+
162
+ const logoRowCount = logo.length;
163
+ const metadataGapColumns = layout.cols >= WIDE_HEADER_MIN_COLUMNS ? 4 : 2;
164
+ const metadataGapRows = mode === "narrow" && metadataRows > 0 ? STACKED_METADATA_GAP_ROWS : 0;
165
+
166
+ const isSideBySide = mode === "wide" || mode === "medium";
167
+
168
+ // In side-by-side mode the right column grows to: update card + gap + metadata.
169
+ // In narrow/stacked mode, the update card is a single extra line below metadata.
170
+ const rightColRows = isSideBySide
171
+ ? (hasUpdate ? UPDATE_CARD_ROWS + UPDATE_CARD_GAP_ROWS : 0) + metadataRows
172
+ : 0;
173
+
174
+ const contentRows = isSideBySide
175
+ ? Math.max(logoRowCount, rightColRows)
176
+ : logoRowCount + metadataGapRows + metadataRows + (hasUpdate ? STACKED_METADATA_GAP_ROWS + 1 : 0);
177
+
178
+ return {
179
+ mode,
180
+ topMarginRows,
181
+ bottomMarginRows,
182
+ metadataGapColumns,
183
+ metadataGapRows,
184
+ logoRows: logoRowCount,
185
+ metadataRows,
186
+ compactHintRows: 0,
187
+ totalRows: topMarginRows + contentRows + bottomMarginRows,
188
+ };
189
+ }
190
+
191
+ export function measureTopHeaderRows(
192
+ layout: Layout,
193
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
194
+ hasUpdate = false,
195
+ ): number {
196
+ return getHeaderHeroLayout(layout, headerConfig, hasUpdate).totalRows;
197
+ }
198
+
199
+ function takeVisualSuffix(text: string, maxWidth: number): string {
200
+ if (maxWidth <= 0) return "";
201
+ let output = "";
202
+
203
+ for (const char of Array.from(text).reverse()) {
204
+ if (getTextWidth(char + output) > maxWidth) break;
205
+ output = char + output;
206
+ }
207
+
208
+ return output;
209
+ }
210
+
211
+ export function shortenHeaderWorkspaceLabel(workspaceLabel: string, maxWidth: number): string {
212
+ const trimmed = workspaceLabel.trim();
213
+ if (!trimmed || maxWidth <= 0) return "";
214
+ if (getTextWidth(trimmed) <= maxWidth) return trimmed;
215
+ if (maxWidth <= 1) return "…";
216
+
217
+ const normalized = trimmed.replace(/[\\/]+$/, "");
218
+ const separatorMatch = normalized.match(/[\\/]/g);
219
+ const separator = normalized.includes("\\") && (!separatorMatch || normalized.lastIndexOf("\\") >= normalized.lastIndexOf("/"))
220
+ ? "\\"
221
+ : "/";
222
+ const lastSlash = Math.max(normalized.lastIndexOf("\\"), normalized.lastIndexOf("/"));
223
+ const leaf = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
224
+ const prefix = lastSlash >= 0 ? `…${separator}` : "…";
225
+ const leafWidth = Math.max(1, maxWidth - getTextWidth(prefix));
226
+
227
+ return prefix + takeVisualSuffix(leaf, leafWidth);
228
+ }
229
+
230
+ function clampMetadataText(text: string, maxWidth: number): string {
231
+ return clampVisualText(text, Math.max(1, maxWidth));
232
+ }
233
+
234
+ export function TopHeader({
235
+ authState,
236
+ workspaceLabel,
237
+ layout,
238
+ runtimeSummary = null,
239
+ headerConfig = HEADER_CONFIG_DEFAULTS,
240
+ updateAvailable = null,
241
+ }: TopHeaderProps) {
242
+ renderDebug.useRenderDebug("Header", {
243
+ authState,
244
+ workspaceLabel,
245
+ cols: layout.cols,
246
+ rows: layout.rows,
247
+ mode: layout.mode,
248
+ });
249
+ renderDebug.useLifecycleDebug("Header", {
250
+ authState,
251
+ cols: layout.cols,
252
+ rows: layout.rows,
253
+ mode: layout.mode,
254
+ });
255
+
256
+ const theme = useTheme();
257
+
258
+ const authLabelRaw = getAuthStateLabel(authState);
259
+ const authLabel = authLabelRaw.length > 0
260
+ ? authLabelRaw[0]!.toUpperCase() + authLabelRaw.slice(1)
261
+ : authLabelRaw;
262
+
263
+ const heroLayout = getHeaderHeroLayout(layout, headerConfig, !!updateAvailable);
264
+ const selectedLogo = selectHeaderLogo(layout);
265
+ const selectedLogoWidth = selectedLogo.length > 0 ? getLogoWidth(selectedLogo) : 0;
266
+
267
+ const contentWidth = getHeaderContentWidth(layout.cols);
268
+ const sideBySideMetadataWidth = Math.max(1, contentWidth - selectedLogoWidth - heroLayout.metadataGapColumns);
269
+ const metadataWidth = heroLayout.mode === "wide" || heroLayout.mode === "medium"
270
+ ? sideBySideMetadataWidth
271
+ : contentWidth;
272
+ const workspaceValueWidth = Math.max(1, metadataWidth - getTextWidth("Workspace: "));
273
+ const wsDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, workspaceValueWidth);
274
+ const brandLabel = formatUbumeBrandLabel();
275
+ const metadataLinesRaw = [
276
+ headerConfig.showBrand ? { key: "brand", text: brandLabel, color: theme.text, bold: true } : null,
277
+ headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.text, bold: false } : null,
278
+ headerConfig.showWorkspace ? { key: "workspace", text: `Workspace: ${wsDisplay}`, color: theme.textMuted, bold: false } : null,
279
+ headerConfig.showProvider && runtimeSummary?.providerLabel
280
+ ? { key: "provider", text: `Provider: ${runtimeSummary.providerLabel}`, color: theme.text, bold: false }
281
+ : null,
282
+ ].filter((line): line is HeaderMetadataLine => Boolean(line));
283
+ const metadataLines = metadataLinesRaw.map((line) => ({
284
+ ...line,
285
+ text: clampMetadataText(line.text, metadataWidth),
286
+ }));
287
+
288
+ // Add a 1-row gap between the version line and workspace line in wide mode
289
+ // so the two pieces of metadata have breathing room beside the logo.
290
+ const hasMetadataGap = heroLayout.mode === "wide"
291
+ && metadataLines.length <= 3
292
+ && metadataLines.some((l) => l.key === "brand")
293
+ && metadataLines.some((l) => l.key === "workspace");
294
+ const metadataColumn = (
295
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} width={metadataWidth}>
296
+ {metadataLines.map((line) =>
297
+ hasMetadataGap && line.key === "workspace" ? (
298
+ <Box key={line.key} marginTop={1}>
299
+ <Text color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
300
+ </Box>
301
+ ) : (
302
+ <Text key={line.key} color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
303
+ )
304
+ )}
305
+ </Box>
306
+ );
307
+
308
+ // Canonical UBUME wordmark — uses per-line LOGO palette defined in each theme.
309
+ // No `bold`: bold on Unicode block/box-drawing characters causes per-glyph
310
+ // spacing artifacts in common terminal fonts (Ptyxis, GNOME Terminal).
311
+ // wrap="truncate" keeps each row on exactly one terminal line.
312
+ const logoColumn = (
313
+ <Box flexDirection="column" flexShrink={0}>
314
+ {selectedLogo.map((line, i) => {
315
+ let lineColor = theme.logoPrimary;
316
+ if (selectedLogo.length === 6) {
317
+ if (i === 2 || i === 3) lineColor = theme.logoSecondary;
318
+ else if (i === 4 || i === 5) lineColor = theme.logoShadow;
319
+ }
320
+ return (
321
+ <Text key={i} color={lineColor} wrap="truncate">{line}</Text>
322
+ );
323
+ })}
324
+ </Box>
325
+ );
326
+
327
+ if (heroLayout.mode !== "compact") {
328
+ const isSideBySide = heroLayout.mode === "wide" || heroLayout.mode === "medium";
329
+ const metadataTopOffset = 0;
330
+
331
+ return (
332
+ <Box flexDirection="column" paddingX={1} width="100%">
333
+ {heroLayout.topMarginRows > 0 && (
334
+ <Box height={heroLayout.topMarginRows} />
335
+ )}
336
+
337
+ {isSideBySide ? (
338
+ <Box flexDirection="row" width="100%" alignItems="flex-start">
339
+ {logoColumn}
340
+ <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
341
+ <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
342
+ {updateAvailable && (
343
+ <>
344
+ <UpdateAvailableCard
345
+ latestVersion={updateAvailable.latestVersion}
346
+ currentVersion={updateAvailable.currentVersion}
347
+ updateCommand={updateAvailable.updateCommand}
348
+ width={metadataWidth}
349
+ />
350
+ <Box height={UPDATE_CARD_GAP_ROWS} />
351
+ </>
352
+ )}
353
+ {metadataColumn}
354
+ </Box>
355
+ </Box>
356
+ ) : (
357
+ <Box flexDirection="column" width="100%">
358
+ {logoColumn}
359
+ {heroLayout.metadataGapRows > 0 && (
360
+ <Box height={heroLayout.metadataGapRows} />
361
+ )}
362
+ {metadataColumn}
363
+ {updateAvailable && (
364
+ <Text color={theme.warning} wrap="truncate">{`Update available: Ubume ${formatVersionLabel(updateAvailable.latestVersion)} — Run: ${updateAvailable.updateCommand ?? UBUME_UPDATE_COMMAND}`}</Text>
365
+ )}
366
+ </Box>
367
+ )}
368
+
369
+ {heroLayout.bottomMarginRows > 0 && (
370
+ <Box height={heroLayout.bottomMarginRows} />
371
+ )}
372
+ </Box>
373
+ );
374
+ }
375
+
376
+ // Compact / micro / activity-collapsed: single-line header.
377
+ const compactMetadataWidth = contentWidth;
378
+ const compactWorkspaceValueWidth = Math.max(1, compactMetadataWidth - getTextWidth("Workspace: "));
379
+ const compactWorkspaceDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, compactWorkspaceValueWidth);
380
+ // A leading ✦ accent makes the single-line header read as a deliberate
381
+ // compact Ubume header rather than a broken fallback.
382
+ const compactParts: React.ReactNode[] = [
383
+ <Text key="accent" color={theme.accent} bold>{"✦ "}</Text>,
384
+ ];
385
+ if (headerConfig.showBrand) {
386
+ compactParts.push(
387
+ <Text key="brand" color={theme.text} bold>{brandLabel}</Text>,
388
+ );
389
+ }
390
+ if (headerConfig.showAuthStatus) {
391
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-auth" color={theme.textDim}>{" · "}</Text>);
392
+ compactParts.push(<Text key="auth" color={theme.text}>{authLabel}</Text>);
393
+ }
394
+ if (headerConfig.showWorkspace) {
395
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-ws" color={theme.textDim}>{" · "}</Text>);
396
+ compactParts.push(<Text key="ws" color={theme.textMuted} wrap="truncate">{`Workspace: ${compactWorkspaceDisplay}`}</Text>);
397
+ }
398
+ if (headerConfig.showProvider && runtimeSummary?.providerLabel) {
399
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-provider" color={theme.textDim}>{" · "}</Text>);
400
+ compactParts.push(<Text key="provider" color={theme.text} wrap="truncate">{`Provider: ${runtimeSummary.providerLabel}`}</Text>);
401
+ }
402
+
403
+ return (
404
+ <Box flexDirection="column" paddingX={1} width="100%">
405
+ {heroLayout.topMarginRows > 0 && (
406
+ <Box height={heroLayout.topMarginRows} />
407
+ )}
408
+ <Box flexDirection="row" width="100%">
409
+ {compactParts}
410
+ </Box>
411
+ {heroLayout.compactHintRows > 0 && (
412
+ <Text color={theme.textDim} wrap="truncate">{clampMetadataText(RECOMMENDED_FULL_HEADER_HINT, compactMetadataWidth)}</Text>
413
+ )}
414
+ {heroLayout.bottomMarginRows > 0 && (
415
+ <Box height={heroLayout.bottomMarginRows} />
416
+ )}
417
+ </Box>
418
+ );
419
+ }
420
+
421
+ // Memoize to prevent re-renders during streaming when props haven't changed
422
+ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
423
+ return (
424
+ prev.authState === next.authState &&
425
+ prev.workspaceLabel === next.workspaceLabel &&
426
+ prev.layout.cols === next.layout.cols &&
427
+ prev.layout.rows === next.layout.rows &&
428
+ prev.layout.mode === next.layout.mode &&
429
+ prev.runtimeSummary === next.runtimeSummary &&
430
+ prev.headerConfig === next.headerConfig &&
431
+ prev.updateAvailable === next.updateAvailable
432
+ );
433
+ });
434
+
435
+ // Minimum terminal cols to render any logo art (the compact 1-row variant).
436
+ export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
437
+
438
+ // Re-export for consumers that reference these constants directly.
439
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "../render/logoVariants.js";
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { UBUME_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
4
+ import { clampVisualText } from "../layout.js";
5
+ import { useTheme } from "../theme.js";
6
+
7
+ export const UPDATE_CARD_CONTENT_ROWS = 4; // title + available + using + command
8
+ export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bottom border rows
9
+
10
+ export interface UpdateAvailableCardProps {
11
+ latestVersion: string;
12
+ currentVersion: string;
13
+ updateCommand?: string;
14
+ /** Total box width including borders. Long lines are truncated to fit. */
15
+ width?: number;
16
+ }
17
+
18
+ export function UpdateAvailableCard({ latestVersion, currentVersion, updateCommand = UBUME_UPDATE_COMMAND, width }: UpdateAvailableCardProps) {
19
+ const theme = useTheme();
20
+ const command = `Run: ${updateCommand}`;
21
+ // Inner content width = boxWidth - 2 (left/right border cols)
22
+ const innerWidth = width !== undefined ? Math.max(8, width - 2) : undefined;
23
+
24
+ function clamp(text: string): string {
25
+ return innerWidth !== undefined ? clampVisualText(text, innerWidth) : text;
26
+ }
27
+
28
+ return (
29
+ <Box
30
+ borderStyle="round"
31
+ borderColor={theme.accent}
32
+ flexDirection="column"
33
+ width={width}
34
+ flexShrink={0}
35
+ >
36
+ <Text color={theme.text} bold>{clamp("Update available")}</Text>
37
+ <Text color={theme.textMuted}>{clamp(`Ubume ${formatVersionLabel(latestVersion)}`)}</Text>
38
+ <Text color={theme.textMuted}>{clamp(`Using ${formatVersionLabel(currentVersion)}`)}</Text>
39
+ <Text color={theme.textDim}>{clamp(command)}</Text>
40
+ </Box>
41
+ );
42
+ }
@@ -0,0 +1,11 @@
1
+ export const BUSY_STATUS_FRAME_MS = 900;
2
+ export const BUSY_STATUS_FRAMES = [" . ", " .. ", " ..."] as const;
3
+
4
+ export function getBusyStatusFrame(frameIndex: number): string {
5
+ const safeIndex = Math.max(0, Math.floor(frameIndex));
6
+ return BUSY_STATUS_FRAMES[safeIndex % BUSY_STATUS_FRAMES.length]!;
7
+ }
8
+
9
+ export function isAnimatedBusyState(kind: string): boolean {
10
+ return kind === "THINKING" || kind === "RESPONDING" || kind === "SHELL_RUNNING";
11
+ }
@@ -0,0 +1,66 @@
1
+ import { formatTerminalAnswerInline } from "../render/terminalAnswerFormat.js";
2
+
3
+ function stripOuterQuotes(s: string): string {
4
+ if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
5
+ return s.slice(1, -1);
6
+ }
7
+ return s;
8
+ }
9
+
10
+ function decodeEscapedQuotes(s: string): string {
11
+ return s.replace(/\\"/g, '"');
12
+ }
13
+
14
+ function cleanCommand(command: string): string {
15
+ return formatTerminalAnswerInline(decodeEscapedQuotes(command.trim()));
16
+ }
17
+
18
+ /**
19
+ * Strips shell-wrapper invocations from a command string and returns the inner
20
+ * command that is actually being executed.
21
+ *
22
+ * Handles:
23
+ * "C:\Program Files\PowerShell\7\pwsh.exe" -Command '...'
24
+ * pwsh.exe -Command "..."
25
+ * powershell.exe -Command '...'
26
+ * cmd.exe /c "..."
27
+ * bash -lc '...'
28
+ */
29
+ export function normalizeCommand(command: string): string {
30
+ // Full quoted path ending with pwsh.exe / powershell.exe
31
+ let match = command.match(/^"[^"]*(?:pwsh|powershell)(?:\.exe)?"\s+-Command\s+(.+)$/si);
32
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
33
+
34
+ // Bare pwsh.exe / powershell.exe on PATH
35
+ match = command.match(/^(?:pwsh|powershell)(?:\.exe)?\s+-Command\s+(.+)$/si);
36
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
37
+
38
+ // cmd.exe /c "..."
39
+ match = command.match(/^cmd(?:\.exe)?\s+\/[cC]\s+(.+)$/s);
40
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
41
+
42
+ // bash -lc "..." or bash -lc '...'
43
+ match = command.match(/^bash\s+-lc\s+(.+)$/s);
44
+ if (match) return cleanCommand(stripOuterQuotes(match[1].trim()));
45
+
46
+ return cleanCommand(command);
47
+ }
48
+
49
+ /**
50
+ * Maps a normalized command to a short human-readable label.
51
+ * Returns null when no label applies and the command itself should be shown.
52
+ */
53
+ export function getFriendlyActionLabel(normalizedCommand: string): string | null {
54
+ const cmd = normalizedCommand.trim();
55
+
56
+ if (/^(?:Get-ChildItem|dir|ls)\b/i.test(cmd)) return "List files";
57
+ if (/^rg\s+--files\b/i.test(cmd)) return "List files";
58
+ if (/^(?:Get-Content|cat)\b/i.test(cmd)) return "Read file";
59
+ if (/^git\s+status\b/.test(cmd)) return "Check git status";
60
+ if (/^git\s+diff\b/.test(cmd)) return "Inspect changes";
61
+ if (/^(?:bun|npm)\s+install\b/.test(cmd)) return "Install dependencies";
62
+ if (/^(?:bun|npm)\s+test\b/.test(cmd)) return "Run tests";
63
+ if (/^(?:tsc\b|bun\s+run\s+typecheck\b)/.test(cmd)) return "Run typecheck";
64
+
65
+ return null;
66
+ }
@@ -0,0 +1,73 @@
1
+ import type { Screen } from "../../session/types.js";
2
+
3
+ export const FOCUS_IDS = {
4
+ composer: "composer",
5
+ resumePicker: "resume-picker",
6
+ planReviewPanel: "plan-review-panel",
7
+ backendPicker: "backend-picker",
8
+ providerPicker: "provider-picker",
9
+ providerSetup: "provider-setup",
10
+ modelPicker: "model-picker",
11
+ modePicker: "mode-picker",
12
+ reasoningPicker: "reasoning-picker",
13
+ themePicker: "theme-picker",
14
+ settingsPanel: "settings-panel",
15
+ authPanel: "auth-panel",
16
+ permissionsPanel: "permissions-panel",
17
+ permissionsApprovalPicker: "permissions-approval-picker",
18
+ permissionsSandboxPicker: "permissions-sandbox-picker",
19
+ permissionsNetworkPicker: "permissions-network-picker",
20
+ permissionsAddWritableRoot: "permissions-add-writable-root",
21
+ permissionsRemoveWritableRoot: "permissions-remove-writable-root",
22
+ importConfirmationPanel: "import-confirmation",
23
+ toolApprovalPanel: "tool-approval",
24
+ updatePrompt: "update-prompt",
25
+ } as const;
26
+
27
+ export type FocusTargetId = (typeof FOCUS_IDS)[keyof typeof FOCUS_IDS];
28
+
29
+ export function getFocusTargetForScreen(screen: Screen): FocusTargetId {
30
+ switch (screen) {
31
+ case "resume-picker":
32
+ return FOCUS_IDS.resumePicker;
33
+ case "backend-picker":
34
+ return FOCUS_IDS.backendPicker;
35
+ case "provider-picker":
36
+ return FOCUS_IDS.providerPicker;
37
+ case "provider-setup":
38
+ return FOCUS_IDS.providerSetup;
39
+ case "model-picker":
40
+ return FOCUS_IDS.modelPicker;
41
+ case "mode-picker":
42
+ return FOCUS_IDS.modePicker;
43
+ case "reasoning-picker":
44
+ return FOCUS_IDS.reasoningPicker;
45
+ case "theme-picker":
46
+ return FOCUS_IDS.themePicker;
47
+ case "settings-panel":
48
+ return FOCUS_IDS.settingsPanel;
49
+ case "auth-panel":
50
+ return FOCUS_IDS.authPanel;
51
+ case "permissions-panel":
52
+ return FOCUS_IDS.permissionsPanel;
53
+ case "permissions-approval-picker":
54
+ return FOCUS_IDS.permissionsApprovalPicker;
55
+ case "permissions-sandbox-picker":
56
+ return FOCUS_IDS.permissionsSandboxPicker;
57
+ case "permissions-network-picker":
58
+ return FOCUS_IDS.permissionsNetworkPicker;
59
+ case "permissions-add-writable-root":
60
+ return FOCUS_IDS.permissionsAddWritableRoot;
61
+ case "permissions-remove-writable-root":
62
+ return FOCUS_IDS.permissionsRemoveWritableRoot;
63
+ case "import-confirmation":
64
+ return FOCUS_IDS.importConfirmationPanel;
65
+ case "tool-approval":
66
+ return FOCUS_IDS.toolApprovalPanel;
67
+ case "update-prompt":
68
+ return FOCUS_IDS.updatePrompt;
69
+ case "main":
70
+ default:
71
+ return FOCUS_IDS.composer;
72
+ }
73
+ }
@@ -0,0 +1,18 @@
1
+ import type { ProviderImageAttachment } from "../../core/providerRuntime/types.js";
2
+ import { createAtomicContentToken, IMAGE_ATTACHMENT_PATTERN } from "./pastedContent.js";
3
+
4
+ export type ImageAttachmentRegistry = Map<string, ProviderImageAttachment>;
5
+
6
+ export function createImageAttachmentToken(attachment: ProviderImageAttachment): string {
7
+ return createAtomicContentToken(`[Image: ${attachment.name}]`);
8
+ }
9
+
10
+ export function selectImageAttachments(value: string, registry: ImageAttachmentRegistry): ProviderImageAttachment[] {
11
+ IMAGE_ATTACHMENT_PATTERN.lastIndex = 0;
12
+ const attachments: ProviderImageAttachment[] = [];
13
+ for (const match of value.matchAll(IMAGE_ATTACHMENT_PATTERN)) {
14
+ const attachment = registry.get(match[0]);
15
+ if (attachment) attachments.push(attachment);
16
+ }
17
+ return attachments;
18
+ }