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,392 @@
1
+ import stringWidth from "string-width";
2
+
3
+ interface WindowSlice {
4
+ text: string;
5
+ cursorColumn: number;
6
+ }
7
+
8
+ export interface TextUnit {
9
+ text: string;
10
+ start: number;
11
+ end: number;
12
+ width: number;
13
+ }
14
+
15
+ export interface WrappedTextRow {
16
+ text: string;
17
+ start: number;
18
+ end: number;
19
+ breakType: "soft" | "hard" | "end";
20
+ }
21
+
22
+ // ─── Character measurement ───────────────────────────────────────────────────
23
+
24
+ export function getCharWidth(char: string): number {
25
+ return Math.max(1, stringWidth(char));
26
+ }
27
+
28
+ export function normalizeLineBreaks(text: string): string {
29
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
30
+ }
31
+
32
+ export function getTextUnits(text: string): TextUnit[] {
33
+ const units: TextUnit[] = [];
34
+ let offset = 0;
35
+
36
+ for (const char of text) {
37
+ const length = char.length;
38
+ units.push({
39
+ text: char,
40
+ start: offset,
41
+ end: offset + length,
42
+ width: getCharWidth(char),
43
+ });
44
+ offset += length;
45
+ }
46
+
47
+ return units;
48
+ }
49
+
50
+ export function getTextWidth(text: string): number {
51
+ return stringWidth(text);
52
+ }
53
+
54
+ function trimToWidthFromEnd(text: string, maxWidth: number): string {
55
+ if (maxWidth <= 0) return "";
56
+ let width = 0;
57
+ const kept: TextUnit[] = [];
58
+ const units = getTextUnits(text);
59
+
60
+ for (let index = units.length - 1; index >= 0; index -= 1) {
61
+ const unit = units[index]!;
62
+ if (width + unit.width > maxWidth) break;
63
+ kept.unshift(unit);
64
+ width += unit.width;
65
+ }
66
+
67
+ return kept.map((unit) => unit.text).join("");
68
+ }
69
+
70
+ function trimToWidthFromStart(text: string, maxWidth: number): string {
71
+ if (maxWidth <= 0) return "";
72
+ let width = 0;
73
+ let output = "";
74
+
75
+ for (const unit of getTextUnits(text)) {
76
+ if (width + unit.width > maxWidth) break;
77
+ output += unit.text;
78
+ width += unit.width;
79
+ }
80
+
81
+ return output;
82
+ }
83
+
84
+ // ─── Input window ────────────────────────────────────────────────────────────
85
+
86
+ export function flattenInputForDisplay(text: string, cursor: number): { text: string; cursor: number } {
87
+ const normalized = normalizeLineBreaks(text);
88
+ const units = getTextUnits(normalized);
89
+ let output = "";
90
+ let mappedCursor = 0;
91
+
92
+ for (const unit of units) {
93
+ if (unit.start === cursor) {
94
+ mappedCursor = output.length;
95
+ }
96
+
97
+ if (unit.text === "\n") {
98
+ output += " ↩ ";
99
+ continue;
100
+ }
101
+ if (unit.text === "\t") {
102
+ output += " ";
103
+ continue;
104
+ }
105
+ output += unit.text;
106
+ }
107
+
108
+ if (cursor >= normalized.length) {
109
+ mappedCursor = output.length;
110
+ }
111
+
112
+ return { text: output, cursor: mappedCursor };
113
+ }
114
+
115
+ export function createInlineInputWindow(text: string, cursor: number, maxWidth: number): WindowSlice {
116
+ const safeWidth = Math.max(1, maxWidth);
117
+ const flattened = flattenInputForDisplay(text, cursor);
118
+ const units = getTextUnits(flattened.text);
119
+ const charStartWidths: number[] = [];
120
+ let totalWidth = 0;
121
+
122
+ for (const unit of units) {
123
+ charStartWidths.push(totalWidth);
124
+ totalWidth += unit.width;
125
+ }
126
+
127
+ const cursorWidth = getTextWidth(flattened.text.slice(0, flattened.cursor));
128
+ if (totalWidth <= safeWidth) {
129
+ return { text: flattened.text, cursorColumn: cursorWidth };
130
+ }
131
+
132
+ const preferredStart = Math.max(0, cursorWidth - Math.floor(safeWidth * 0.65));
133
+ let windowStart = preferredStart;
134
+ if (windowStart + safeWidth > totalWidth) {
135
+ windowStart = Math.max(0, totalWidth - safeWidth);
136
+ }
137
+ const windowEnd = windowStart + safeWidth;
138
+
139
+ let startIndex = 0;
140
+ while (startIndex < units.length && charStartWidths[startIndex]! + units[startIndex]!.width <= windowStart) {
141
+ startIndex += 1;
142
+ }
143
+
144
+ let endIndex = startIndex;
145
+ while (endIndex < units.length && charStartWidths[endIndex]! < windowEnd) {
146
+ endIndex += 1;
147
+ }
148
+
149
+ let visibleText = units.slice(startIndex, endIndex).map((unit) => unit.text).join("");
150
+ let cursorColumn = Math.max(0, cursorWidth - (charStartWidths[startIndex] ?? 0));
151
+ const truncatedLeft = startIndex > 0;
152
+ const truncatedRight = endIndex < units.length;
153
+
154
+ if (truncatedLeft) {
155
+ const ellipsis = "…";
156
+ const available = Math.max(1, safeWidth - getCharWidth(ellipsis) - (truncatedRight ? getCharWidth(ellipsis) : 0));
157
+ visibleText = ellipsis + trimToWidthFromEnd(visibleText, available);
158
+ cursorColumn = Math.min(getTextWidth(visibleText), Math.max(getCharWidth(ellipsis), cursorColumn + getCharWidth(ellipsis)));
159
+ }
160
+
161
+ if (truncatedRight) {
162
+ const ellipsis = "…";
163
+ const available = Math.max(1, safeWidth - (truncatedLeft ? getCharWidth(ellipsis) : 0) - getCharWidth(ellipsis));
164
+ const baseText = truncatedLeft ? visibleText.slice(1) : visibleText;
165
+ visibleText = `${truncatedLeft ? "…" : ""}${trimToWidthFromStart(baseText, available)}${ellipsis}`;
166
+ }
167
+
168
+ return {
169
+ text: visibleText,
170
+ cursorColumn: Math.max(0, Math.min(getTextWidth(visibleText), cursorColumn)),
171
+ };
172
+ }
173
+
174
+ export function splitTextAtColumn(text: string, column: number): { before: string; current: string; after: string } {
175
+ const safeColumn = Math.max(0, column);
176
+ let width = 0;
177
+ const units = getTextUnits(text);
178
+
179
+ for (const unit of units) {
180
+ if (width + unit.width > safeColumn) {
181
+ return {
182
+ before: text.slice(0, unit.start),
183
+ current: unit.text,
184
+ after: text.slice(unit.end),
185
+ };
186
+ }
187
+
188
+ width += unit.width;
189
+ if (width > safeColumn) break;
190
+ }
191
+
192
+ return {
193
+ before: text,
194
+ current: "",
195
+ after: "",
196
+ };
197
+ }
198
+
199
+ // ─── Text wrapping ────────────────────────────────────────────────────────────
200
+
201
+ export function wrapTextRows(
202
+ text: string,
203
+ maxWidth: number,
204
+ firstLineWidth: number = maxWidth,
205
+ ): WrappedTextRow[] {
206
+ const normalized = normalizeLineBreaks(text);
207
+ const safeWidth = Math.max(1, maxWidth);
208
+ const safeFirstWidth = Math.max(1, firstLineWidth);
209
+ const rows: WrappedTextRow[] = [];
210
+ let rowUnits: TextUnit[] = [];
211
+ let rowWidth = 0;
212
+ let skippingSoftWhitespace = false;
213
+
214
+ const unitsText = (units: TextUnit[]) => units.map((unit) => unit.text).join("");
215
+ const unitsWidth = (units: TextUnit[]) => units.reduce((total, current) => total + current.width, 0);
216
+ const pushRow = (units: TextUnit[], end: number, breakType: WrappedTextRow["breakType"]) => {
217
+ rows.push({
218
+ text: unitsText(units),
219
+ start: units[0]?.start ?? end,
220
+ end,
221
+ breakType,
222
+ });
223
+ };
224
+ const findWordBoundary = (units: TextUnit[]): { breakStart: number; continuationStart: number } | null => {
225
+ for (let index = units.length - 2; index >= 0; index -= 1) {
226
+ if (!/^[ \t]$/.test(units[index]!.text)) continue;
227
+
228
+ let breakStart = index;
229
+ while (breakStart > 0 && /^[ \t]$/.test(units[breakStart - 1]!.text)) {
230
+ breakStart -= 1;
231
+ }
232
+
233
+ let continuationStart = index + 1;
234
+ while (continuationStart < units.length && /^[ \t]$/.test(units[continuationStart]!.text)) {
235
+ continuationStart += 1;
236
+ }
237
+
238
+ if (breakStart > 0 && continuationStart < units.length) {
239
+ return { breakStart, continuationStart };
240
+ }
241
+ index = breakStart;
242
+ }
243
+ return null;
244
+ };
245
+
246
+ for (const unit of getTextUnits(normalized)) {
247
+ if (unit.text === "\n") {
248
+ pushRow(rowUnits, unit.start, "hard");
249
+ rowUnits = [];
250
+ rowWidth = 0;
251
+ skippingSoftWhitespace = false;
252
+ continue;
253
+ }
254
+
255
+ if (skippingSoftWhitespace && /^[ \t]$/.test(unit.text)) continue;
256
+ skippingSoftWhitespace = false;
257
+
258
+ // Only the first emitted row honors firstLineWidth; later rows use maxWidth.
259
+ const limit = rows.length === 0 ? safeFirstWidth : safeWidth;
260
+ if (rowUnits.length > 0 && rowWidth + unit.width > limit) {
261
+ const candidate = [...rowUnits, unit];
262
+ const boundary = findWordBoundary(candidate);
263
+ if (boundary) {
264
+ const before = candidate.slice(0, boundary.breakStart);
265
+ const after = candidate.slice(boundary.continuationStart);
266
+ pushRow(before, candidate[boundary.breakStart]!.start, "soft");
267
+ rowUnits = after;
268
+ rowWidth = unitsWidth(after);
269
+ } else if (/^[ \t]$/.test(unit.text)) {
270
+ pushRow(rowUnits, unit.start, "soft");
271
+ rowUnits = [];
272
+ rowWidth = 0;
273
+ skippingSoftWhitespace = true;
274
+ } else {
275
+ pushRow(rowUnits, unit.start, "soft");
276
+ rowUnits = [unit];
277
+ rowWidth = unit.width;
278
+ }
279
+ continue;
280
+ }
281
+
282
+ rowUnits.push(unit);
283
+ rowWidth += unit.width;
284
+ }
285
+
286
+ if (rowUnits.length > 0) {
287
+ pushRow(rowUnits, normalized.length, "end");
288
+ } else {
289
+ rows.push({
290
+ text: "",
291
+ start: normalized.length,
292
+ end: normalized.length,
293
+ breakType: "end",
294
+ });
295
+ }
296
+
297
+ return rows.length > 0 ? rows : [{
298
+ text: "",
299
+ start: 0,
300
+ end: 0,
301
+ breakType: "end",
302
+ }];
303
+ }
304
+
305
+ export function wrapPlainText(
306
+ text: string,
307
+ maxWidth: number,
308
+ firstLineWidth: number = maxWidth,
309
+ ): string[] {
310
+ return wrapTextRows(text, maxWidth, firstLineWidth).map((row) => row.text);
311
+ }
312
+
313
+ export function wrapCommandText(text: string, maxWidth: number): string[] {
314
+ if (maxWidth <= 2) return [];
315
+ const normalized = normalizeLineBreaks(text);
316
+ const rows: string[] = [];
317
+ let currentLine = "";
318
+ let currentWidth = 0;
319
+
320
+ // Split on whitespace, but keep the whitespace tokens
321
+ const tokens = normalized.split(/([ \t]+)/);
322
+
323
+ for (let i = 0; i < tokens.length; i++) {
324
+ let token = tokens[i];
325
+ if (!token) continue;
326
+
327
+ if (token === "\n") {
328
+ rows.push(currentLine);
329
+ currentLine = " ";
330
+ currentWidth = 2;
331
+ continue;
332
+ }
333
+
334
+ let tokenWidth = getTextWidth(token);
335
+
336
+ // Skip leading whitespace on continuation lines
337
+ if (/^[ \t]+$/.test(token) && currentLine === " ") {
338
+ continue;
339
+ }
340
+
341
+ if (currentWidth + tokenWidth > maxWidth) {
342
+ if (/^[ \t]+$/.test(token)) {
343
+ // Space that pushes us over the edge, ignore it and break
344
+ rows.push(currentLine);
345
+ currentLine = " ";
346
+ currentWidth = 2;
347
+ continue;
348
+ }
349
+
350
+ if (currentWidth > 2) {
351
+ // We have some content on this line, push it and start a new line
352
+ rows.push(currentLine);
353
+ currentLine = " ";
354
+ currentWidth = 2;
355
+ }
356
+
357
+ // Now check if the token alone exceeds the available width (maxWidth - 2)
358
+ while (tokenWidth > maxWidth - currentWidth) {
359
+ const available = maxWidth - currentWidth;
360
+ const split = splitTextAtColumn(token, available);
361
+
362
+ currentLine += split.before;
363
+ rows.push(currentLine);
364
+
365
+ token = split.current + split.after;
366
+ tokenWidth = getTextWidth(token);
367
+ currentLine = " ";
368
+ currentWidth = 2;
369
+ }
370
+
371
+ currentLine += token;
372
+ currentWidth += tokenWidth;
373
+
374
+ } else {
375
+ currentLine += token;
376
+ currentWidth += tokenWidth;
377
+ }
378
+ }
379
+
380
+ if (currentLine.trim().length > 0 || currentLine === " ") {
381
+ // only push if there is actual content, or if it is an intentionally empty line (rare)
382
+ if (currentLine !== " ") {
383
+ rows.push(currentLine);
384
+ }
385
+ }
386
+
387
+ if (rows.length > 0 && rows[0].startsWith(" ") && !text.startsWith(" ")) {
388
+ rows[0] = rows[0].substring(2);
389
+ }
390
+
391
+ return rows;
392
+ }
@@ -0,0 +1,274 @@
1
+ import React, { createContext, useContext, ReactNode } from "react";
2
+ import * as renderDebug from "../core/perf/renderDebug.js";
3
+
4
+ export interface Theme {
5
+ bg: string;
6
+ surface: string;
7
+ surfaceMuted: string;
8
+ border: string;
9
+ borderFocused: string;
10
+ text: string;
11
+ textMuted: string;
12
+ textDim: string;
13
+ accent: string;
14
+ accentMuted: string;
15
+ success: string;
16
+ warning: string;
17
+ error: string;
18
+ info: string;
19
+ command: string;
20
+ prompt: string;
21
+ model: string;
22
+ provider: string;
23
+ context: string;
24
+ logoPrimary: string;
25
+ logoSecondary: string;
26
+ logoShadow: string;
27
+ }
28
+
29
+ // ─── Theme definitions ────────────────────────────────────────────────────────
30
+
31
+ export const DARK_THEME = {
32
+ bg: "#0E0E10",
33
+ surface: "#18181B",
34
+ surfaceMuted: "#111113",
35
+ border: "#27272A",
36
+ borderFocused: "#2DD4BF",
37
+ accent: "#10B981",
38
+ accentMuted: "#059669",
39
+ text: "#F4F4F5",
40
+ textMuted: "#A1A1AA",
41
+ textDim: "#52525B",
42
+ success: "#22C55E",
43
+ warning: "#F59E0B",
44
+ error: "#EF4444",
45
+ info: "#06B6D4",
46
+ command: "#34D399",
47
+ prompt: "#10B981",
48
+ model: "#60A5FA",
49
+ provider: "#6EE7B7",
50
+ context: "#67E8F9",
51
+ logoPrimary: "#34D399",
52
+ logoSecondary: "#10B981",
53
+ logoShadow: "#064E3B",
54
+ } satisfies Theme;
55
+
56
+ export const PURPLE_THEME = {
57
+ bg: "#12101C",
58
+ surface: "#1B162E",
59
+ surfaceMuted: "#151224",
60
+ border: "#3B2C66",
61
+ borderFocused: "#BB9AF7",
62
+ accent: "#BB9AF7",
63
+ accentMuted: "#9D78E3",
64
+ text: "#E2D9FA",
65
+ textMuted: "#9AA5CE",
66
+ textDim: "#565F89",
67
+ success: "#9ECE6A",
68
+ warning: "#E0AF68",
69
+ error: "#F7768E",
70
+ info: "#7AA2F7",
71
+ command: "#BB9AF7",
72
+ prompt: "#7DCFFF",
73
+ model: "#89DDFF",
74
+ provider: "#B4F9F8",
75
+ context: "#89DDFF",
76
+ logoPrimary: "#BB9AF7",
77
+ logoSecondary: "#7AA2F7",
78
+ logoShadow: "#3B2C66",
79
+ } satisfies Theme;
80
+
81
+ export const MONO_THEME = {
82
+ bg: "#0F0F0F",
83
+ surface: "#1A1A1A",
84
+ surfaceMuted: "#141414",
85
+ border: "#333333",
86
+ borderFocused: "#FAFAFA",
87
+ accent: "#FAFAFA",
88
+ accentMuted: "#A3A3A3",
89
+ text: "#F5F5F5",
90
+ textMuted: "#A3A3A3",
91
+ textDim: "#525252",
92
+ success: "#D4D4D4",
93
+ warning: "#A3A3A3",
94
+ error: "#E5E5E5",
95
+ info: "#8A8A8A",
96
+ command: "#E5E5E5",
97
+ prompt: "#D4D4D4",
98
+ model: "#E5E5E5",
99
+ provider: "#A3A3A3",
100
+ context: "#A3A3A3",
101
+ logoPrimary: "#FAFAFA",
102
+ logoSecondary: "#D4D4D4",
103
+ logoShadow: "#525252",
104
+ } satisfies Theme;
105
+
106
+ export const BLACK_THEME = {
107
+ bg: "#000000",
108
+ surface: "#0D0D10",
109
+ surfaceMuted: "#060608",
110
+ border: "#242427",
111
+ borderFocused: "#FAFAFA",
112
+ accent: "#F4F4F5",
113
+ accentMuted: "#A1A1AA",
114
+ text: "#FAFAFA",
115
+ textMuted: "#A1A1AA",
116
+ textDim: "#52525B",
117
+ success: "#10B981",
118
+ warning: "#F59E0B",
119
+ error: "#EF4444",
120
+ info: "#3B82F6",
121
+ command: "#FAFAFA",
122
+ prompt: "#D4D4D8",
123
+ model: "#60A5FA",
124
+ provider: "#A7F3D0",
125
+ context: "#67E8F9",
126
+ logoPrimary: "#FAFAFA",
127
+ logoSecondary: "#A1A1AA",
128
+ logoShadow: "#52525B",
129
+ } satisfies Theme;
130
+
131
+ export const NORDIC_THEME = {
132
+ bg: "#1E222A",
133
+ surface: "#2E3440",
134
+ surfaceMuted: "#242933",
135
+ border: "#3B4252",
136
+ borderFocused: "#88C0D0",
137
+ accent: "#8FBCBB",
138
+ accentMuted: "#81A1C1",
139
+ text: "#ECEFF4",
140
+ textMuted: "#D8DEE9",
141
+ textDim: "#4C566A",
142
+ success: "#A3BE8C",
143
+ warning: "#EBCB8B",
144
+ error: "#BF616A",
145
+ info: "#5E81AC",
146
+ command: "#88C0D0",
147
+ prompt: "#88C0D0",
148
+ model: "#81A1C1",
149
+ provider: "#8FBCBB",
150
+ context: "#88C0D0",
151
+ logoPrimary: "#88C0D0",
152
+ logoSecondary: "#81A1C1",
153
+ logoShadow: "#434C5E",
154
+ } satisfies Theme;
155
+
156
+ export const DRACULA_THEME = {
157
+ bg: "#1E1F29",
158
+ surface: "#282A36",
159
+ surfaceMuted: "#21222C",
160
+ border: "#44475A",
161
+ borderFocused: "#BD93F9",
162
+ accent: "#FF79C6",
163
+ accentMuted: "#BF93F9",
164
+ text: "#F8F8F2",
165
+ textMuted: "#BFBFBF",
166
+ textDim: "#6272A4",
167
+ success: "#50FA7B",
168
+ warning: "#F1FA8C",
169
+ error: "#FF5555",
170
+ info: "#8BE9FD",
171
+ command: "#FF79C6",
172
+ prompt: "#8BE9FD",
173
+ model: "#BD93F9",
174
+ provider: "#8BE9FD",
175
+ context: "#8BE9FD",
176
+ logoPrimary: "#BD93F9",
177
+ logoSecondary: "#FF79C6",
178
+ logoShadow: "#44475A",
179
+ } satisfies Theme;
180
+
181
+ export const GRUVBOX_THEME = {
182
+ bg: "#1D2021",
183
+ surface: "#282828",
184
+ surfaceMuted: "#242424",
185
+ border: "#3C3836",
186
+ borderFocused: "#FABD2F",
187
+ accent: "#FABD2F",
188
+ accentMuted: "#D79921",
189
+ text: "#EBDBB2",
190
+ textMuted: "#A89984",
191
+ textDim: "#665C54",
192
+ success: "#B8BB26",
193
+ warning: "#FE8019",
194
+ error: "#FB4934",
195
+ info: "#83A598",
196
+ command: "#FABD2F",
197
+ prompt: "#83A598",
198
+ model: "#83A598",
199
+ provider: "#8EC07C",
200
+ context: "#83A598",
201
+ logoPrimary: "#FABD2F",
202
+ logoSecondary: "#B8BB26",
203
+ logoShadow: "#504945",
204
+ } satisfies Theme;
205
+
206
+ export const OCEAN_THEME = {
207
+ bg: "#030712",
208
+ surface: "#0F172A",
209
+ surfaceMuted: "#0A0F1D",
210
+ border: "#1E293B",
211
+ borderFocused: "#38BDF8",
212
+ accent: "#0EA5E9",
213
+ accentMuted: "#0284C7",
214
+ text: "#F0F9FF",
215
+ textMuted: "#7DD3FC",
216
+ textDim: "#0369A1",
217
+ success: "#10B981",
218
+ warning: "#F59E0B",
219
+ error: "#F43F5E",
220
+ info: "#0EA5E9",
221
+ command: "#0EA5E9",
222
+ prompt: "#38BDF8",
223
+ model: "#7DD3FC",
224
+ provider: "#38BDF8",
225
+ context: "#38BDF8",
226
+ logoPrimary: "#0EA5E9",
227
+ logoSecondary: "#38BDF8",
228
+ logoShadow: "#1E3A70",
229
+ } satisfies Theme;
230
+
231
+ // ─── Theme registry ───────────────────────────────────────────────────────────
232
+
233
+ export const THEMES: Record<string, Theme> = {
234
+ dark: DARK_THEME,
235
+ purple: PURPLE_THEME,
236
+ mono: MONO_THEME,
237
+ black: BLACK_THEME,
238
+ nordic: NORDIC_THEME,
239
+ dracula: DRACULA_THEME,
240
+ gruvbox: GRUVBOX_THEME,
241
+ ocean: OCEAN_THEME,
242
+ };
243
+
244
+ // ─── Context & hook ───────────────────────────────────────────────────────────
245
+
246
+ const ThemeContext = createContext<Theme>(DARK_THEME);
247
+
248
+ interface ThemeProviderProps {
249
+ theme?: string;
250
+ customTheme?: Partial<Theme>;
251
+ children: ReactNode;
252
+ }
253
+
254
+ export function ThemeProvider({ theme: themeName = "dark", customTheme, children }: ThemeProviderProps) {
255
+ renderDebug.useLifecycleDebug("ThemeProvider", {
256
+ themeName,
257
+ customTheme: Boolean(customTheme),
258
+ });
259
+ renderDebug.useRenderDebug("ThemeProvider", {
260
+ themeName,
261
+ customTheme: Boolean(customTheme),
262
+ });
263
+ const baseTheme = THEMES[themeName] || DARK_THEME;
264
+ const activeTheme = themeName === "custom" ? { ...DARK_THEME, ...customTheme } : baseTheme;
265
+ return (
266
+ <ThemeContext.Provider value={activeTheme as Theme}>
267
+ {children}
268
+ </ThemeContext.Provider>
269
+ );
270
+ }
271
+
272
+ export function useTheme() {
273
+ return useContext(ThemeContext);
274
+ }
@@ -0,0 +1,41 @@
1
+ import type { Screen } from "../session/types.js";
2
+
3
+ export interface ThemeSelectionState {
4
+ committedTheme: string;
5
+ previewTheme: string | null;
6
+ }
7
+
8
+ export function getDisplayedThemeName(state: ThemeSelectionState): string {
9
+ return state.previewTheme ?? state.committedTheme;
10
+ }
11
+
12
+ export function previewThemeSelection(
13
+ state: ThemeSelectionState,
14
+ nextTheme: string,
15
+ ): ThemeSelectionState {
16
+ return {
17
+ ...state,
18
+ previewTheme: nextTheme,
19
+ };
20
+ }
21
+
22
+ export function commitThemeSelection(
23
+ state: ThemeSelectionState,
24
+ nextTheme: string,
25
+ ): ThemeSelectionState {
26
+ return {
27
+ committedTheme: nextTheme,
28
+ previewTheme: null,
29
+ };
30
+ }
31
+
32
+ export function cancelThemeSelection(state: ThemeSelectionState): ThemeSelectionState {
33
+ return {
34
+ committedTheme: state.committedTheme,
35
+ previewTheme: null,
36
+ };
37
+ }
38
+
39
+ export function shouldBumpComposerInstance(previousScreen: Screen, nextScreen: Screen): boolean {
40
+ return previousScreen !== "main" && nextScreen === "main";
41
+ }