@golba98/codexa 1.0.2 → 1.0.4

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 (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -36,7 +36,7 @@ import {
36
36
  type WorkspaceDisplayMode,
37
37
  } from "../config/settings.js";
38
38
 
39
- import type { WorkspaceCommandContext } from "../core/launchContext.js";
39
+ import type { WorkspaceCommandContext } from "../core/workspace/launchContext.js";
40
40
  import {
41
41
  findModelCapability,
42
42
  formatModelCapabilitiesList,
@@ -120,7 +120,7 @@ export interface CommandContext {
120
120
  modelCapabilities?: CodexModelCapabilities | null;
121
121
  routeStatusMessage?: string;
122
122
  activeRouteProviderLabel?: string;
123
- projectInstructions?: import("../core/projectInstructions.js").ProjectInstructionsLoadResult | null;
123
+ projectInstructions?: import("../core/workspace/projectInstructions.js").ProjectInstructionsLoadResult | null;
124
124
  }
125
125
 
126
126
  // Mirrors AVAILABLE_APPROVAL_POLICIES[].id from runtimeConfig.ts
@@ -398,10 +398,10 @@ function buildHelpMessage(context: CommandContext): string {
398
398
  " /copy Copy last response to clipboard",
399
399
  " /update [check] Show update status and instructions for updating Codexa",
400
400
  " /help Show this help",
401
- "",
402
- "Install on Windows:",
403
- " npm link Make the codexa command available",
404
- " where codexa Verify the command resolves",
401
+ "",
402
+ "Local development:",
403
+ " npm run install:dev-bin Install the codexa-dev command",
404
+ " codexa-dev Run this repo without replacing codexa",
405
405
  "",
406
406
  "Shortcuts:",
407
407
  " Ctrl+B Open backend picker",
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
- export const BUILD_COMMIT = "6ef3a8a85efef3a525472973403a628a84804c4d" as const;
3
- export const BUILD_VERSION = "1.0.2" as const;
2
+ export const BUILD_COMMIT = "b066a15b77696d2a0945ea7c6c3d07630584296b" as const;
3
+ export const APP_VERSION = "1.0.4" as const;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { dirname, join, resolve } from "path";
3
- import { normalizeWorkspaceRoot } from "../core/workspaceRoot.js";
3
+ import { normalizeWorkspaceRoot } from "../core/workspace/workspaceRoot.js";
4
4
  import {
5
5
  formatApprovalPolicyLabel,
6
6
  formatNetworkAccessLabel,
@@ -50,6 +50,7 @@ export interface AuthSettings {
50
50
  export interface UpdateCheckSettings {
51
51
  enabled: boolean;
52
52
  intervalHours: number;
53
+ skippedUpdateVersion?: string | null;
53
54
  }
54
55
 
55
56
  export const DEFAULT_UPDATE_CHECK_SETTINGS: UpdateCheckSettings = {
@@ -125,6 +126,7 @@ function normalizeUpdateCheckSettings(
125
126
  intervalHours: typeof input?.intervalHours === "number" && input.intervalHours > 0
126
127
  ? input.intervalHours
127
128
  : DEFAULT_UPDATE_CHECK_SETTINGS.intervalHours,
129
+ skippedUpdateVersion: input?.skippedUpdateVersion ?? null,
128
130
  };
129
131
  }
130
132
 
@@ -269,6 +271,11 @@ export function parseSettingsData(data: unknown): AppSettings {
269
271
  : typeof updateCheckSource.interval_hours === "number"
270
272
  ? updateCheckSource.interval_hours
271
273
  : undefined,
274
+ skippedUpdateVersion: typeof updateCheckSource.skippedUpdateVersion === "string"
275
+ ? updateCheckSource.skippedUpdateVersion
276
+ : typeof updateCheckSource.skipped_update_version === "string"
277
+ ? updateCheckSource.skipped_update_version
278
+ : null,
272
279
  }),
273
280
  };
274
281
  }
@@ -299,6 +306,9 @@ export function serializeSettings(settings: AppSettings): Record<string, unknown
299
306
  update_check: {
300
307
  enabled: settings.updateCheck.enabled,
301
308
  interval_hours: settings.updateCheck.intervalHours,
309
+ ...(settings.updateCheck.skippedUpdateVersion != null
310
+ ? { skipped_update_version: settings.updateCheck.skippedUpdateVersion }
311
+ : {}),
302
312
  },
303
313
  };
304
314
  }
@@ -106,7 +106,7 @@ export interface ResolvedRuntimeConfig {
106
106
  export interface RuntimeStatusContext {
107
107
  workspaceRoot: string;
108
108
  tokensUsed?: number | null;
109
- projectInstructions?: import("../core/projectInstructions.js").ProjectInstructionsLoadResult | null;
109
+ projectInstructions?: import("../core/workspace/projectInstructions.js").ProjectInstructionsLoadResult | null;
110
110
  }
111
111
 
112
112
  export interface RuntimeSummary {
@@ -1,6 +1,5 @@
1
- import { homedir } from "os";
2
- import { basename, join, parse, win32 } from "path";
3
- import { BUILD_VERSION } from "./buildInfo.js";
1
+ import { homedir } from "os";
2
+ import { basename, join, parse, win32 } from "path";
4
3
 
5
4
  function isWindowsStylePath(p: string): boolean {
6
5
  return /^[A-Za-z]:[\\/]/.test(p) || /^\\\\/.test(p);
@@ -10,14 +9,14 @@ function smartJoin(base: string, ...parts: string[]): string {
10
9
  return isWindowsStylePath(base) ? win32.join(base, ...parts) : join(base, ...parts);
11
10
  }
12
11
 
13
- export const APP_NAME = "Codexa";
14
- export const APP_VERSION = BUILD_VERSION;
12
+ export { APP_VERSION } from "./buildInfo.js";
13
+ export const APP_NAME = "Codexa";
15
14
  export const DEFAULT_BACKEND = "codex-subprocess";
16
15
  export const DEFAULT_MODEL = "gpt-5.4";
17
16
  export const DEFAULT_MODE = "full-auto";
18
17
  export const DEFAULT_REASONING_LEVEL = "high";
19
18
  export const DEFAULT_LAYOUT_STYLE = "gemini-shell";
20
- export const DEFAULT_THEME = "mono";
19
+ export const DEFAULT_THEME = "dark";
21
20
  export const DEFAULT_WORKSPACE_DISPLAY_MODE = "dir";
22
21
  export const DEFAULT_TERMINAL_TITLE_MODE = "dir";
23
22
  export const DEFAULT_SHOW_BUSY_LOADER = true;
@@ -102,7 +101,7 @@ export const TERMINAL_MOUSE_MODES = ["wheel", "selection"] as const;
102
101
 
103
102
  export type TerminalMouseMode = (typeof TERMINAL_MOUSE_MODES)[number];
104
103
 
105
- export const DEFAULT_TERMINAL_MOUSE_MODE: TerminalMouseMode = "selection";
104
+ export const DEFAULT_TERMINAL_MOUSE_MODE: TerminalMouseMode = "wheel";
106
105
 
107
106
  export interface SettingOption<TValue extends string> {
108
107
  value: TValue;
@@ -265,21 +264,14 @@ export function formatReasoningLabel(reasoning: string): string {
265
264
  }
266
265
 
267
266
  export const AVAILABLE_THEMES = [
267
+ { id: "dark", label: "Codexa Dark" },
268
268
  { id: "purple", label: "Midnight Purple" },
269
269
  { id: "mono", label: "Black & White" },
270
- { id: "dark", label: "Modern Dark" },
271
270
  { id: "black", label: "Codex the Black" },
272
- { id: "emerald", label: "Emerald Night" },
273
- { id: "solar", label: "Solar Flare" },
274
- { id: "cyber", label: "Cyberpunk Neon" },
275
- { id: "ocean", label: "Deep Oceanic" },
276
271
  { id: "nordic", label: "Nordic Frost" },
277
- { id: "green", label: "Terminal Green" },
278
- { id: "amber", label: "Terminal Amber" },
279
- { id: "vaporwave", label: "Vaporwave Dream" },
280
272
  { id: "dracula", label: "Dracula Night" },
281
273
  { id: "gruvbox", label: "Gruvbox Hard" },
282
- { id: "synthwave", label: "Synthwave '84" },
274
+ { id: "ocean", label: "Deep Oceanic" },
283
275
  { id: "custom", label: "Customize..." },
284
276
  ] as const;
285
277
 
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
2
  import { dirname } from "path";
3
3
  import { getCodexaTrustStoreFile } from "./settings.js";
4
- import { normalizeWorkspaceRoot } from "../core/workspaceRoot.js";
4
+ import { normalizeWorkspaceRoot } from "../core/workspace/workspaceRoot.js";
5
5
 
6
6
  interface TrustStoreData {
7
7
  trustedProjectRoots: string[];
@@ -39,7 +39,25 @@ export function saveUpdateCheckCache(cache: UpdateCheckCache): void {
39
39
  }
40
40
  }
41
41
 
42
- export function isCacheValid(cache: UpdateCheckCache, intervalHours: number): boolean {
42
+ function stripV(v: string): string {
43
+ return v.startsWith("v") ? v.slice(1) : v;
44
+ }
45
+
46
+ /**
47
+ * Returns true only when the cache is still usable:
48
+ * - `runningVersion` matches the version the cache was written for (version-mismatch = stale)
49
+ * - The cache was written within `intervalHours`
50
+ */
51
+ export function isCacheValid(
52
+ cache: UpdateCheckCache,
53
+ intervalHours: number,
54
+ runningVersion?: string,
55
+ ): boolean {
56
+ if (runningVersion !== undefined) {
57
+ if (stripV(cache.currentVersion) !== stripV(runningVersion)) {
58
+ return false;
59
+ }
60
+ }
43
61
  const maxAgeMs = intervalHours * 60 * 60 * 1000;
44
62
  return Date.now() - cache.lastChecked < maxAgeMs;
45
63
  }
@@ -0,0 +1,52 @@
1
+ # `src/core`
2
+
3
+ Non-UI runtime logic for Codexa: launching backends, talking to provider CLIs,
4
+ terminal I/O, workspace resolution, and supporting utilities. UI lives in `src/ui`,
5
+ app state in `src/session`, config in `src/config`. The top level of `src/core` is
6
+ folders-only — every file lives in a domain folder so the directory stays scannable.
7
+
8
+ ## Folder map
9
+
10
+ | Folder | Responsibility |
11
+ | --- | --- |
12
+ | `providers/` | **Low-level Codex subprocess I/O.** Spawns the `codex` binary and parses its output (`codexSubprocess`, `codexJsonStream`, `codexTranscript`), plus the backend registry/types and the `openaiNative` stub. |
13
+ | `providerRuntime/` | **Multi-provider runtimes + discovery.** One runtime per provider (`anthropic`, `gemini`, `local`, `antigravity`) behind a shared interface, plus routing (`registry`), model/metadata helpers (`models`, `capabilityProfile`, `contextMetadata`, `reasoning`) and Claude Code discovery. |
14
+ | `providerLauncher/` | **Workspace provider config + CLI launching.** Which provider is active per workspace (`workspaceConfig`), provider UI state (`registry`), and spawning provider CLIs (`launcher`). |
15
+ | `codex/` | Codex CLI launch/prompt assembly: `codexExecArgs`, `codexLaunch`, `codexPrompt`. |
16
+ | `models/` | Codex CLI capability discovery (`codexCapabilities`, `codexModelCapabilities`) and the legacy model-spec service (`modelSpecs`). |
17
+ | `executables/` | Resolve external CLI binaries with PATH/env handling (`executableResolver` + per-CLI resolvers). |
18
+ | `auth/` | Codex auth status probing. |
19
+ | `process/` | Generic process spawning (`CommandRunner`) and executable-path validation. |
20
+ | `terminal/` | Terminal I/O: ANSI sanitize, raw mode / cursor, title sequences, capability detection, and the `/clear` + resize repaint boundary (`clearFrameBoundary`, `inkRenderReset`). |
21
+ | `workspace/` | Workspace resolution and state: `workspaceRoot`, `workspaceGuard`, `workspaceActivity`, `projectInstructions`, `planStorage`, `launchContext`. |
22
+ | `version/` | Build channel / version branding (`channel`) and update checking (`updateCheck`). |
23
+ | `shared/` | Small cross-cutting utilities: `clipboard`, `cleanupFastFail`, `githubDiagnostics`, `attachments`, `hollowResponseFormat`. |
24
+ | `perf/` | Performance + render instrumentation (`profiler`, `renderDebug`). |
25
+ | `debug/` | Dev-only tracing helpers (`inputDebug`). |
26
+
27
+ ## The three provider layers
28
+
29
+ `providers/`, `providerRuntime/`, and `providerLauncher/` have similar names but are
30
+ distinct layers — they are **not** duplicates:
31
+
32
+ ```
33
+ providerLauncher/ which provider is active for this workspace + how to spawn its CLI
34
+
35
+ providerRuntime/ per-provider runtimes (anthropic/gemini/local/antigravity),
36
+ │ routing, model discovery, capability/context metadata
37
+
38
+ providers/ low-level Codex subprocess I/O + output parsing
39
+ ```
40
+
41
+ The default Codex backend flows through `providers/`; the other providers are
42
+ implemented as `providerRuntime/` runtimes.
43
+
44
+ ## Debug instrumentation
45
+
46
+ These are intentional, env-gated diagnostics (not dead code) — keep them named clearly:
47
+
48
+ - `debug/inputDebug.ts` — stdin state tracing (`CODEXA_DEBUG_INPUT=1`).
49
+ - `perf/renderDebug.ts` — Ink render/flicker tracing (`CODEXA_RENDER_DEBUG=1`). Kept in
50
+ `perf/` rather than `debug/` because it is imported widely across the UI.
51
+ - `providerRuntime/claudeCodeDiscoveryDebug.ts` — entry point for the
52
+ `bun run debug:claude-models` script; lives next to the discovery code it exercises.
@@ -0,0 +1,282 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { BackendRunHandlers } from "../providers/types.js";
4
+ import type { ProviderChatRequest } from "../providerRuntime/types.js";
5
+ import { executeAgentTool, type AgentToolResult } from "./tools.js";
6
+ import { parseAgentToolCall, serializeToolResult, type NormalizedAgentToolCall } from "./protocol.js";
7
+
8
+ export interface AgentChatMessage {
9
+ role: "system" | "user" | "assistant" | "tool";
10
+ content: string;
11
+ }
12
+
13
+ export interface AgentChatResponse {
14
+ text: string;
15
+ toolCalls?: readonly NormalizedAgentToolCall[];
16
+ }
17
+
18
+ export interface RunAgentLoopOptions {
19
+ request: ProviderChatRequest;
20
+ handlers: BackendRunHandlers;
21
+ sendMessages: (messages: readonly AgentChatMessage[], turnIndex: number) => Promise<AgentChatResponse>;
22
+ includeSystemPrompt: boolean;
23
+ signal?: AbortSignal;
24
+ maxToolCalls?: number;
25
+ }
26
+
27
+ const DEFAULT_MAX_TOOL_CALLS = 10;
28
+
29
+ function workspaceSummary(workspaceRoot: string): string {
30
+ const lines = [`Workspace root: ${workspaceRoot}`];
31
+ try {
32
+ const entries = readdirSync(workspaceRoot, { withFileTypes: true })
33
+ .filter((entry) => ![".git", "node_modules", "dist", "build", "coverage"].includes(entry.name))
34
+ .map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`)
35
+ .sort()
36
+ .slice(0, 20);
37
+ if (entries.length > 0) lines.push(`Top-level entries: ${entries.join(", ")}`);
38
+ } catch {
39
+ // Tool-based inspection remains available when a shallow summary is unavailable.
40
+ }
41
+
42
+ try {
43
+ const packageJsonPath = path.join(workspaceRoot, "package.json");
44
+ if (existsSync(packageJsonPath)) {
45
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as Record<string, unknown>;
46
+ const name = typeof packageJson.name === "string" ? packageJson.name : null;
47
+ const description = typeof packageJson.description === "string" ? packageJson.description : null;
48
+ if (name) lines.push(`Package: ${name}${description ? ` - ${description}` : ""}`);
49
+ }
50
+ } catch {
51
+ // A malformed package.json should not prevent a local chat request.
52
+ }
53
+
54
+ return lines.join("\n");
55
+ }
56
+
57
+ function localAgentSystemPrompt(request: ProviderChatRequest): string {
58
+ const hasCargoToml = existsSync(path.join(request.workspaceRoot, "Cargo.toml"));
59
+ return [
60
+ `You are an autonomous coding assistant running inside this workspace: ${request.workspaceRoot}`,
61
+ "You must inspect files with tools before claiming you cannot see them.",
62
+ "For broad questions about the repository, use the workspace summary below, then inspect with get_workspace_info or list_files before answering when more detail is needed.",
63
+ "Use tools to create, edit, build, and test when the user asks for workspace changes.",
64
+ "Do not ask vague clarification questions when the user's intent has an obvious safe implementation.",
65
+ hasCargoToml
66
+ ? "Rust workspace note: Cargo.toml exists. Prefer src/main.rs for simple binaries, use cargo check for validation, use cargo run for running, and do not use rustc main.rs unless main.rs is truly at the workspace root."
67
+ : null,
68
+ "Use exactly one tool call at a time in this format:",
69
+ '<tool_call>{"name":"read_file","arguments":{"path":"src/index.tsx"}}</tool_call>',
70
+ "Available tools: list_files, read_file, write_file, apply_patch, run_shell, get_workspace_info.",
71
+ "Summarize changed files and commands run in your final answer.",
72
+ `Workspace summary:\n${workspaceSummary(request.workspaceRoot)}`,
73
+ request.projectInstructions?.content
74
+ ? ["Project instructions:", request.projectInstructions.content].join("\n")
75
+ : null,
76
+ ].filter(Boolean).join("\n\n");
77
+ }
78
+
79
+ function buildInitialMessages(request: ProviderChatRequest, includeSystemPrompt: boolean): AgentChatMessage[] {
80
+ const systemPrompt = localAgentSystemPrompt(request);
81
+ if (includeSystemPrompt) {
82
+ return [
83
+ { role: "system", content: systemPrompt },
84
+ { role: "user", content: request.prompt },
85
+ ];
86
+ }
87
+
88
+ return [
89
+ { role: "user", content: `${systemPrompt}\n\nUser request:\n${request.prompt}` },
90
+ ];
91
+ }
92
+
93
+ function toolActivityCommand(result: Pick<AgentToolResult, "tool" | "path" | "paths" | "command">): string {
94
+ if (result.command) return `${result.tool}: ${result.command}`;
95
+ if (result.path) return `${result.tool}: ${result.path}`;
96
+ if (result.paths && result.paths.length > 0) return `${result.tool}: ${result.paths.join(", ")}`;
97
+ return result.tool;
98
+ }
99
+
100
+ interface ExecutedCommand {
101
+ command: string;
102
+ success: boolean;
103
+ exitCode?: number | null;
104
+ durationMs?: number;
105
+ }
106
+
107
+ interface AgentLoopSummary {
108
+ changedFiles: Set<string>;
109
+ commands: ExecutedCommand[];
110
+ toolResults: AgentToolResult[];
111
+ }
112
+
113
+ function stableJson(value: unknown): string {
114
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
115
+ if (value && typeof value === "object") {
116
+ const record = value as Record<string, unknown>;
117
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
118
+ }
119
+ return JSON.stringify(value);
120
+ }
121
+
122
+ function toolCallSignature(call: Pick<NormalizedAgentToolCall, "name" | "arguments">): string {
123
+ return `${call.name}:${stableJson(call.arguments)}`;
124
+ }
125
+
126
+ function recordToolResult(summary: AgentLoopSummary, result: AgentToolResult): void {
127
+ for (const file of result.paths ?? []) {
128
+ if (file) summary.changedFiles.add(file);
129
+ }
130
+ if (result.path && (result.tool === "write_file" || result.tool === "apply_patch")) {
131
+ summary.changedFiles.add(result.path);
132
+ }
133
+ if (result.command) {
134
+ summary.commands.push({
135
+ command: result.command,
136
+ success: result.success,
137
+ exitCode: result.exitCode,
138
+ durationMs: result.durationMs,
139
+ });
140
+ }
141
+ summary.toolResults.push(result);
142
+ }
143
+
144
+ function commandStatus(command: ExecutedCommand): string {
145
+ const status = command.success ? "succeeded" : "failed";
146
+ const exitCode = command.exitCode === undefined ? "" : `, exit ${command.exitCode ?? "n/a"}`;
147
+ return `- ${command.command}: ${status}${exitCode}`;
148
+ }
149
+
150
+ function nextCommand(request: ProviderChatRequest, summary: AgentLoopSummary): string {
151
+ if (existsSync(path.join(request.workspaceRoot, "Cargo.toml"))) return "cargo run";
152
+ const lastValidation = [...summary.commands].reverse().find((item) =>
153
+ /\b(?:cargo check|cargo run|bun test|npm test|bun run typecheck|tsc)\b/.test(item.command)
154
+ );
155
+ if (lastValidation) return lastValidation.command;
156
+ return "bun test";
157
+ }
158
+
159
+ function synthesizeFinalMessage(request: ProviderChatRequest, summary: AgentLoopSummary, reason: string): string {
160
+ const files = [...summary.changedFiles].sort();
161
+ const commandLines = summary.commands.map(commandStatus);
162
+ return [
163
+ reason,
164
+ "",
165
+ "Files changed:",
166
+ files.length > 0 ? files.map((file) => `- ${file}`).join("\n") : "- None detected",
167
+ "",
168
+ "Commands run:",
169
+ commandLines.length > 0 ? commandLines.join("\n") : "- None",
170
+ "",
171
+ `Next command: ${nextCommand(request, summary)}`,
172
+ ].join("\n").trim();
173
+ }
174
+
175
+ async function requestFinalAnswer(options: RunAgentLoopOptions, messages: AgentChatMessage[], toolCallCount: number, reason: string): Promise<string | null> {
176
+ messages.push({
177
+ role: "user",
178
+ content: [
179
+ reason,
180
+ "Stop calling tools now. Write the final answer using the tool results already provided.",
181
+ "Include files changed, commands run, whether they succeeded, and the next command the user can run.",
182
+ ].join("\n"),
183
+ });
184
+ const response = await options.sendMessages(messages, toolCallCount);
185
+ const parsed = parseAgentToolCall(response.text);
186
+ return parsed.kind === "final" && parsed.text.trim() ? parsed.text.trim() : null;
187
+ }
188
+
189
+ export async function runAgentLoop(options: RunAgentLoopOptions): Promise<string> {
190
+ const maxToolCalls = options.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
191
+ const messages = buildInitialMessages(options.request, options.includeSystemPrompt);
192
+ let toolCallCount = 0;
193
+ let previousToolSignature: string | null = null;
194
+ const summary: AgentLoopSummary = {
195
+ changedFiles: new Set(),
196
+ commands: [],
197
+ toolResults: [],
198
+ };
199
+
200
+ while (true) {
201
+ if (options.signal?.aborted) {
202
+ throw new Error("Local agent run was canceled.");
203
+ }
204
+
205
+ const response = await options.sendMessages(messages, toolCallCount);
206
+ const structuredToolCall = response.toolCalls?.[0] ?? null;
207
+ const parsed = structuredToolCall
208
+ ? { kind: "tool_call" as const, ...structuredToolCall, raw: JSON.stringify(structuredToolCall) }
209
+ : parseAgentToolCall(response.text);
210
+
211
+ if (parsed.kind === "final") {
212
+ return parsed.text.trim();
213
+ }
214
+
215
+ if (toolCallCount >= maxToolCalls) {
216
+ const reason = `Local agent reached ${maxToolCalls} tool calls without a final answer.`;
217
+ const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
218
+ return final ?? synthesizeFinalMessage(options.request, summary, reason);
219
+ }
220
+
221
+ if (parsed.kind === "malformed_tool_call") {
222
+ messages.push({ role: "assistant", content: response.text });
223
+ toolCallCount += 1;
224
+ messages.push({
225
+ role: "user",
226
+ content: serializeToolResult({
227
+ success: false,
228
+ error: `Malformed tool call: ${parsed.error}`,
229
+ raw: parsed.raw,
230
+ }),
231
+ });
232
+ continue;
233
+ }
234
+
235
+ const signature = toolCallSignature(parsed);
236
+ if (signature === previousToolSignature) {
237
+ const reason = `Local agent repeated the same ${parsed.name} tool call with identical arguments.`;
238
+ const final = await requestFinalAnswer(options, messages, toolCallCount, reason);
239
+ return final ?? synthesizeFinalMessage(options.request, summary, reason);
240
+ }
241
+
242
+ messages.push({ role: "assistant", content: response.text || parsed.raw });
243
+ previousToolSignature = signature;
244
+ toolCallCount += 1;
245
+ const activityId = `local-agent-${toolCallCount}-${parsed.name}`;
246
+ const startedAt = Date.now();
247
+ const runningCommand = toolActivityCommand({
248
+ tool: parsed.name,
249
+ path: typeof parsed.arguments.path === "string" ? parsed.arguments.path : undefined,
250
+ command: typeof parsed.arguments.command === "string" ? parsed.arguments.command : undefined,
251
+ });
252
+ options.handlers.onToolActivity?.({
253
+ id: activityId,
254
+ command: runningCommand,
255
+ status: "running",
256
+ startedAt,
257
+ });
258
+
259
+ const result = await executeAgentTool(parsed.name, parsed.arguments, {
260
+ workspaceRoot: options.request.workspaceRoot,
261
+ runtime: options.request.runtime,
262
+ signal: options.signal,
263
+ });
264
+ const completedCommand = toolActivityCommand(result);
265
+ options.handlers.onToolActivity?.({
266
+ id: activityId,
267
+ command: completedCommand,
268
+ status: result.success ? "completed" : "failed",
269
+ startedAt,
270
+ completedAt: Date.now(),
271
+ summary: result.summary ?? result.error ?? null,
272
+ });
273
+ recordToolResult(summary, result);
274
+
275
+ messages.push({
276
+ role: "user",
277
+ content: serializeToolResult(result),
278
+ });
279
+ }
280
+
281
+ throw new Error(`Local agent stopped after ${maxToolCalls} tool calls without a final answer.`);
282
+ }