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,16 @@
1
+ const LEGACY_ENV_PREFIX = "CODEXA_";
2
+ const ENV_PREFIX = "UBUME_";
3
+ // Codexa remains the name of the native model family, so its variables are not aliased.
4
+ const MODEL_FAMILY_ENV = /^CODEXA_(NATIVE|CUPY|NUMPY)_/;
5
+
6
+ /**
7
+ * Pre-rename `CODEXA_*` variables keep working: copy each one into its
8
+ * `UBUME_*` name unless that name is already set. Mutates `env` in place.
9
+ */
10
+ export function applyLegacyEnvAliases(env: NodeJS.ProcessEnv = process.env): void {
11
+ for (const [key, value] of Object.entries(env)) {
12
+ if (!key.startsWith(LEGACY_ENV_PREFIX) || MODEL_FAMILY_ENV.test(key)) continue;
13
+ const ubumeKey = `${ENV_PREFIX}${key.slice(LEGACY_ENV_PREFIX.length)}`;
14
+ if (env[ubumeKey] === undefined) env[ubumeKey] = value;
15
+ }
16
+ }
@@ -0,0 +1,377 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { Theme } from "../ui/theme.js";
4
+ import {
5
+ AUTH_PREFERENCES,
6
+ DEFAULT_AUTH_PREFERENCE,
7
+ DEFAULT_LAYOUT_STYLE,
8
+ DEFAULT_SHOW_BUSY_LOADER,
9
+ DEFAULT_TERMINAL_TITLE_MODE,
10
+ DEFAULT_THEME,
11
+ DEFAULT_WORKSPACE_DISPLAY_MODE,
12
+ HEADER_CONFIG_DEFAULTS,
13
+ LEGACY_DIRECTORY_DISPLAY_MODES,
14
+ WORKSPACE_DISPLAY_MODES,
15
+ getCodexConfigFile,
16
+ normalizeLegacyDirectoryDisplayMode,
17
+ SETTINGS_FILE,
18
+ LEGACY_SETTINGS_FILE,
19
+ type AuthPreference,
20
+ type HeaderConfig,
21
+ type TerminalTitleMode,
22
+ type WorkspaceDisplayMode,
23
+ } from "./settings.js";
24
+ import {
25
+ mergeRuntimeIntoTomlConfig,
26
+ parseTomlDocument,
27
+ } from "./layeredConfig.js";
28
+ import { serializeTomlDocument } from "./toml-serialize.js";
29
+ import {
30
+ normalizeRuntimeConfig,
31
+ type RuntimeConfig,
32
+ } from "./runtimeConfig.js";
33
+ import type { AvailableMode } from "./settings.js";
34
+
35
+ export interface UiSettings {
36
+ layoutStyle: string;
37
+ theme: string;
38
+ workspaceDisplayMode: WorkspaceDisplayMode;
39
+ terminalTitleMode: TerminalTitleMode;
40
+ showBusyLoader: boolean;
41
+ customTheme?: Partial<Theme>;
42
+ }
43
+
44
+ export interface AuthSettings {
45
+ preference: AuthPreference;
46
+ }
47
+
48
+ export interface UpdateCheckSettings {
49
+ enabled: boolean;
50
+ intervalHours: number;
51
+ skippedUpdateVersion?: string | null;
52
+ }
53
+
54
+ export const DEFAULT_UPDATE_CHECK_SETTINGS: UpdateCheckSettings = {
55
+ enabled: true,
56
+ intervalHours: 6,
57
+ };
58
+
59
+ export interface AppSettings {
60
+ ui: UiSettings;
61
+ auth: AuthSettings;
62
+ header: HeaderConfig;
63
+ updateCheck: UpdateCheckSettings;
64
+ }
65
+
66
+ // Pick the first string value found under the camelCase or snake_case key.
67
+ // Falls through to the snake_case key only when the camelCase value is absent or not a string.
68
+ function pickStr(src: Record<string, unknown>, camel: string, snake: string): string | undefined {
69
+ const camelVal = src[camel];
70
+ if (typeof camelVal === "string") return camelVal;
71
+ const snakeVal = src[snake];
72
+ if (typeof snakeVal === "string") return snakeVal;
73
+ return undefined;
74
+ }
75
+
76
+ // Same as pickStr but for boolean values.
77
+ function pickBool(src: Record<string, unknown>, camel: string, snake: string): boolean | undefined {
78
+ const camelVal = src[camel];
79
+ if (typeof camelVal === "boolean") return camelVal;
80
+ const snakeVal = src[snake];
81
+ if (typeof snakeVal === "boolean") return snakeVal;
82
+ return undefined;
83
+ }
84
+
85
+ function normalizeAuthPreference(value: unknown): AuthPreference {
86
+ return typeof value === "string" && AUTH_PREFERENCES.some((item) => item.id === value)
87
+ ? (value as AuthPreference)
88
+ : DEFAULT_AUTH_PREFERENCE;
89
+ }
90
+
91
+ function normalizeUiSettings(input: Partial<UiSettings> | null | undefined): UiSettings {
92
+ return {
93
+ layoutStyle: input?.layoutStyle ?? DEFAULT_LAYOUT_STYLE,
94
+ theme: input?.theme ?? DEFAULT_THEME,
95
+ workspaceDisplayMode: input?.workspaceDisplayMode ?? DEFAULT_WORKSPACE_DISPLAY_MODE,
96
+ terminalTitleMode: input?.terminalTitleMode ?? DEFAULT_TERMINAL_TITLE_MODE,
97
+ showBusyLoader: typeof input?.showBusyLoader === "boolean"
98
+ ? input.showBusyLoader
99
+ : DEFAULT_SHOW_BUSY_LOADER,
100
+ customTheme: input?.customTheme,
101
+ };
102
+ }
103
+
104
+ function normalizeHeaderConfig(input: Partial<HeaderConfig> | null | undefined): HeaderConfig {
105
+ return {
106
+ showBrand: typeof input?.showBrand === "boolean" ? input.showBrand : HEADER_CONFIG_DEFAULTS.showBrand,
107
+ showWorkspace: typeof input?.showWorkspace === "boolean" ? input.showWorkspace : HEADER_CONFIG_DEFAULTS.showWorkspace,
108
+ showProvider: typeof input?.showProvider === "boolean" ? input.showProvider : HEADER_CONFIG_DEFAULTS.showProvider,
109
+ showModel: typeof input?.showModel === "boolean" ? input.showModel : HEADER_CONFIG_DEFAULTS.showModel,
110
+ showReasoning: typeof input?.showReasoning === "boolean" ? input.showReasoning : HEADER_CONFIG_DEFAULTS.showReasoning,
111
+ showContext: typeof input?.showContext === "boolean" ? input.showContext : HEADER_CONFIG_DEFAULTS.showContext,
112
+ showAuthStatus: typeof input?.showAuthStatus === "boolean" ? input.showAuthStatus : HEADER_CONFIG_DEFAULTS.showAuthStatus,
113
+ };
114
+ }
115
+
116
+ function normalizeUpdateCheckSettings(
117
+ input: Partial<UpdateCheckSettings> | null | undefined,
118
+ ): UpdateCheckSettings {
119
+ return {
120
+ enabled: typeof input?.enabled === "boolean" ? input.enabled : DEFAULT_UPDATE_CHECK_SETTINGS.enabled,
121
+ intervalHours: typeof input?.intervalHours === "number" && input.intervalHours > 0
122
+ ? input.intervalHours
123
+ : DEFAULT_UPDATE_CHECK_SETTINGS.intervalHours,
124
+ skippedUpdateVersion: input?.skippedUpdateVersion ?? null,
125
+ };
126
+ }
127
+
128
+ export function getDefaultSettings(): AppSettings {
129
+ return {
130
+ ui: normalizeUiSettings(null),
131
+ auth: {
132
+ preference: DEFAULT_AUTH_PREFERENCE,
133
+ },
134
+ header: normalizeHeaderConfig(null),
135
+ updateCheck: normalizeUpdateCheckSettings(null),
136
+ };
137
+ }
138
+
139
+ function parseLegacyRuntime(data: Record<string, unknown>): RuntimeConfig {
140
+ return normalizeRuntimeConfig({
141
+ provider: typeof data.backend === "string" ? data.backend as RuntimeConfig["provider"] : undefined,
142
+ model: typeof data.model === "string" ? data.model as RuntimeConfig["model"] : undefined,
143
+ mode: typeof data.mode === "string" ? data.mode as RuntimeConfig["mode"] : undefined,
144
+ reasoningLevel: typeof data.reasoning_level === "string" ? data.reasoning_level as RuntimeConfig["reasoningLevel"] : undefined,
145
+ });
146
+ }
147
+
148
+ export function extractLegacyRuntime(data: unknown): RuntimeConfig | null {
149
+ if (!data || typeof data !== "object") {
150
+ return null;
151
+ }
152
+
153
+ const record = data as Record<string, unknown>;
154
+ if (typeof record.runtime === "object" && record.runtime !== null) {
155
+ return normalizeRuntimeConfig(record.runtime as Partial<RuntimeConfig>);
156
+ }
157
+
158
+ const hasFlatRuntimeKeys = ["backend", "model", "mode", "reasoning_level"].some((key) => key in record);
159
+ return hasFlatRuntimeKeys ? parseLegacyRuntime(record) : null;
160
+ }
161
+
162
+ function stripLegacyRuntime(data: unknown): Record<string, unknown> {
163
+ if (!data || typeof data !== "object") {
164
+ return {};
165
+ }
166
+
167
+ const record = { ...(data as Record<string, unknown>) };
168
+ delete record.runtime;
169
+ delete record.backend;
170
+ delete record.model;
171
+ delete record.mode;
172
+ delete record.reasoning_level;
173
+ return record;
174
+ }
175
+
176
+ function parseTerminalTitleMode(uiSource: Record<string, unknown>, fallback: TerminalTitleMode): TerminalTitleMode {
177
+ const direct = uiSource.terminalTitleMode ?? uiSource.terminal_title_mode;
178
+ if (typeof direct === "string" && WORKSPACE_DISPLAY_MODES.includes(direct as WorkspaceDisplayMode)) {
179
+ return direct as TerminalTitleMode;
180
+ }
181
+
182
+ return fallback;
183
+ }
184
+
185
+ function parseWorkspaceDisplayMode(uiSource: Record<string, unknown>, fallback: WorkspaceDisplayMode): WorkspaceDisplayMode {
186
+ const direct = uiSource.workspaceDisplayMode ?? uiSource.workspace_display_mode;
187
+ if (typeof direct === "string" && WORKSPACE_DISPLAY_MODES.includes(direct as WorkspaceDisplayMode)) {
188
+ return direct as WorkspaceDisplayMode;
189
+ }
190
+
191
+ const legacy = uiSource.directoryDisplayMode ?? uiSource.directory_display_mode;
192
+ if (typeof legacy === "string") {
193
+ if (WORKSPACE_DISPLAY_MODES.includes(legacy as WorkspaceDisplayMode)) {
194
+ return legacy as WorkspaceDisplayMode;
195
+ }
196
+ if (LEGACY_DIRECTORY_DISPLAY_MODES.includes(legacy as typeof LEGACY_DIRECTORY_DISPLAY_MODES[number])) {
197
+ return normalizeLegacyDirectoryDisplayMode(legacy as typeof LEGACY_DIRECTORY_DISPLAY_MODES[number]);
198
+ }
199
+ }
200
+
201
+ return fallback;
202
+ }
203
+
204
+ export function parseSettingsData(data: unknown): AppSettings {
205
+ const defaults = getDefaultSettings();
206
+ if (!data || typeof data !== "object") {
207
+ return defaults;
208
+ }
209
+
210
+ const record = data as Record<string, unknown>;
211
+ const uiSource = typeof record.ui === "object" && record.ui !== null
212
+ ? record.ui as Record<string, unknown>
213
+ : record;
214
+ const authSource = typeof record.auth === "object" && record.auth !== null
215
+ ? record.auth as Record<string, unknown>
216
+ : record;
217
+
218
+ const headerSource = typeof record.header === "object" && record.header !== null
219
+ ? record.header as Record<string, unknown>
220
+ : {};
221
+
222
+ const updateCheckSource = typeof record.updateCheck === "object" && record.updateCheck !== null
223
+ ? record.updateCheck as Record<string, unknown>
224
+ : typeof record.update_check === "object" && record.update_check !== null
225
+ ? record.update_check as Record<string, unknown>
226
+ : {};
227
+
228
+ return {
229
+ ui: normalizeUiSettings({
230
+ layoutStyle: pickStr(uiSource, "layoutStyle", "layout_style") ?? defaults.ui.layoutStyle,
231
+ theme: pickStr(uiSource, "theme", "theme") ?? defaults.ui.theme,
232
+ workspaceDisplayMode: parseWorkspaceDisplayMode(uiSource, defaults.ui.workspaceDisplayMode),
233
+ terminalTitleMode: parseTerminalTitleMode(uiSource, defaults.ui.terminalTitleMode),
234
+ showBusyLoader: pickBool(uiSource, "showBusyLoader", "show_busy_loader") ?? defaults.ui.showBusyLoader,
235
+ customTheme: (uiSource.customTheme ?? uiSource.custom_theme) as Partial<Theme> | undefined,
236
+ }),
237
+ auth: {
238
+ preference: normalizeAuthPreference(authSource.preference ?? authSource.auth_preference),
239
+ },
240
+ header: normalizeHeaderConfig({
241
+ showBrand: pickBool(headerSource, "showBrand", "show_brand"),
242
+ showWorkspace: pickBool(headerSource, "showWorkspace", "show_workspace"),
243
+ showProvider: pickBool(headerSource, "showProvider", "show_provider"),
244
+ showModel: pickBool(headerSource, "showModel", "show_model"),
245
+ showReasoning: pickBool(headerSource, "showReasoning", "show_reasoning"),
246
+ showContext: pickBool(headerSource, "showContext", "show_context"),
247
+ showAuthStatus: pickBool(headerSource, "showAuthStatus", "show_auth_status"),
248
+ }),
249
+ updateCheck: normalizeUpdateCheckSettings({
250
+ enabled: pickBool(updateCheckSource, "enabled", "enabled"),
251
+ intervalHours: typeof updateCheckSource.intervalHours === "number"
252
+ ? updateCheckSource.intervalHours
253
+ : typeof updateCheckSource.interval_hours === "number"
254
+ ? updateCheckSource.interval_hours
255
+ : undefined,
256
+ skippedUpdateVersion: typeof updateCheckSource.skippedUpdateVersion === "string"
257
+ ? updateCheckSource.skippedUpdateVersion
258
+ : typeof updateCheckSource.skipped_update_version === "string"
259
+ ? updateCheckSource.skipped_update_version
260
+ : null,
261
+ }),
262
+ };
263
+ }
264
+
265
+ export function serializeSettings(settings: AppSettings): Record<string, unknown> {
266
+ return {
267
+ ui: {
268
+ layout_style: settings.ui.layoutStyle,
269
+ theme: settings.ui.theme,
270
+ workspace_display_mode: settings.ui.workspaceDisplayMode,
271
+ terminal_title_mode: settings.ui.terminalTitleMode,
272
+ show_busy_loader: settings.ui.showBusyLoader,
273
+ custom_theme: settings.ui.customTheme,
274
+ },
275
+ auth: {
276
+ preference: settings.auth.preference,
277
+ },
278
+ header: {
279
+ show_brand: settings.header.showBrand,
280
+ show_workspace: settings.header.showWorkspace,
281
+ show_provider: settings.header.showProvider,
282
+ show_model: settings.header.showModel,
283
+ show_reasoning: settings.header.showReasoning,
284
+ show_context: settings.header.showContext,
285
+ show_auth_status: settings.header.showAuthStatus,
286
+ },
287
+ update_check: {
288
+ enabled: settings.updateCheck.enabled,
289
+ interval_hours: settings.updateCheck.intervalHours,
290
+ ...(settings.updateCheck.skippedUpdateVersion != null
291
+ ? { skipped_update_version: settings.updateCheck.skippedUpdateVersion }
292
+ : {}),
293
+ },
294
+ };
295
+ }
296
+
297
+ function writeJsonFile(filePath: string, data: Record<string, unknown>): void {
298
+ mkdirSync(dirname(filePath), { recursive: true });
299
+ const tmpFile = `${filePath}.tmp`;
300
+ writeFileSync(tmpFile, JSON.stringify(data, null, 2), "utf-8");
301
+ renameSync(tmpFile, filePath);
302
+ }
303
+
304
+ function maybeMigrateLegacyRuntime(rawData: unknown): void {
305
+ const legacyRuntime = extractLegacyRuntime(rawData);
306
+ if (!legacyRuntime) {
307
+ return;
308
+ }
309
+
310
+ try {
311
+ const codexConfigFile = getCodexConfigFile();
312
+ const existingConfig = existsSync(codexConfigFile)
313
+ ? parseTomlDocument(readFileSync(codexConfigFile, "utf-8"))
314
+ : {};
315
+ const mergedConfig = mergeRuntimeIntoTomlConfig(existingConfig, legacyRuntime);
316
+ mkdirSync(dirname(codexConfigFile), { recursive: true });
317
+ const tmpTomlFile = `${codexConfigFile}.tmp`;
318
+ writeFileSync(tmpTomlFile, serializeTomlDocument(mergedConfig), "utf-8");
319
+ renameSync(tmpTomlFile, codexConfigFile);
320
+ writeJsonFile(SETTINGS_FILE, stripLegacyRuntime(rawData));
321
+ } catch {
322
+ // Best-effort migration only; keep legacy JSON intact if anything fails.
323
+ }
324
+ }
325
+
326
+ export function loadSettings(): AppSettings {
327
+ try {
328
+ let text = "";
329
+ if (existsSync(SETTINGS_FILE)) {
330
+ text = readFileSync(SETTINGS_FILE, "utf-8");
331
+ } else if (existsSync(LEGACY_SETTINGS_FILE)) {
332
+ text = readFileSync(LEGACY_SETTINGS_FILE, "utf-8");
333
+ try {
334
+ writeJsonFile(SETTINGS_FILE, JSON.parse(text));
335
+ } catch {
336
+ // Best-effort migration only
337
+ }
338
+ } else {
339
+ return getDefaultSettings();
340
+ }
341
+ const rawData = JSON.parse(text);
342
+ maybeMigrateLegacyRuntime(rawData);
343
+ return parseSettingsData(rawData);
344
+ } catch {
345
+ return getDefaultSettings();
346
+ }
347
+ }
348
+
349
+ export function saveSettings(settings: AppSettings): void {
350
+ try {
351
+ writeJsonFile(SETTINGS_FILE, serializeSettings(settings));
352
+ } catch {
353
+ // Silently ignore — settings are best-effort
354
+ }
355
+ }
356
+
357
+ /** Persist the interactive execution choice without replacing unrelated Codex config. */
358
+ export function saveRuntimeModePreference(mode: AvailableMode, planMode: boolean): void {
359
+ try {
360
+ const codexConfigFile = getCodexConfigFile();
361
+ const current = existsSync(codexConfigFile)
362
+ ? parseTomlDocument(readFileSync(codexConfigFile, "utf-8"))
363
+ : {};
364
+ const useCodexa = !current.ubume && current.codexa && typeof current.codexa === "object" && !Array.isArray(current.codexa);
365
+ const tableKey = useCodexa ? "codexa" : "ubume";
366
+ const existing = (current[tableKey] && typeof current[tableKey] === "object" && !Array.isArray(current[tableKey]))
367
+ ? { ...(current[tableKey] as Record<string, unknown>) }
368
+ : {};
369
+ current[tableKey] = { ...existing, mode, plan_mode: planMode };
370
+ mkdirSync(dirname(codexConfigFile), { recursive: true });
371
+ const tmpFile = `${codexConfigFile}.tmp`;
372
+ writeFileSync(tmpFile, serializeTomlDocument(current), "utf-8");
373
+ renameSync(tmpFile, codexConfigFile);
374
+ } catch {
375
+ // Runtime preference persistence is best-effort and must not crash the TUI.
376
+ }
377
+ }