@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,103 @@
1
+ import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
2
+ import type { ProjectInstructions } from "../projectInstructions.js";
3
+ import type { BackendRunHandlers } from "../providers/types.js";
4
+ import type { ResolvedRuntimeConfig } from "../../config/runtimeConfig.js";
5
+ export type { ResolvedRuntimeConfig };
6
+ import type { ProviderId } from "../providerLauncher/types.js";
7
+ import type { ProviderWorkspaceOverride } from "../providerLauncher/types.js";
8
+
9
+ export type ProviderBackendKind =
10
+ | "codex-cli-auth"
11
+ | "gemini-cli-auth"
12
+ | "claude-code-auth"
13
+ | "openai-api-key"
14
+ | "gemini-api-key"
15
+ | "anthropic-api-key"
16
+ | "local-openai-compatible"
17
+ | "unavailable";
18
+
19
+ export interface ProviderModel {
20
+ id: string;
21
+ modelId: string;
22
+ label: string;
23
+ description: string | null;
24
+ defaultReasoningLevel: string | null;
25
+ supportedReasoningLevels: readonly ReasoningEffortCapability[] | null;
26
+ source?: "discovered" | "claude-code" | "settings" | "config" | "fallback";
27
+ canonicalId?: string;
28
+ family?: string;
29
+ effortSource?: "claude-code" | "settings" | "config" | "fallback";
30
+ effortVerified?: boolean;
31
+ raw?: unknown;
32
+ }
33
+
34
+ export interface ProviderModelDiscoveryResult {
35
+ status: "ready" | "not-configured";
36
+ providerId: ProviderId;
37
+ backendKind: ProviderBackendKind;
38
+ models: readonly ProviderModel[];
39
+ message?: string;
40
+ diagnostics?: Record<string, string | number | boolean | null>;
41
+ }
42
+
43
+ export type GeminiModelFamily = "gemini-3" | "gemini-2.5";
44
+
45
+ export type GeminiModelSelection =
46
+ | { kind: "auto"; family: GeminiModelFamily }
47
+ | { kind: "manual"; modelId: string };
48
+
49
+ export interface ProviderRoute {
50
+ providerId: ProviderId;
51
+ modelId: string;
52
+ backendKind: ProviderBackendKind;
53
+ reasoning?: string;
54
+ modelSelection?: GeminiModelSelection;
55
+ }
56
+
57
+ export type ActiveProviderRoute = ProviderRoute;
58
+
59
+ export interface ProviderRouteValidationRequest {
60
+ route: ProviderRoute;
61
+ workspaceRoot: string;
62
+ geminiCommandPath?: string | null;
63
+ claudeCommandPath?: string | null;
64
+ localConfig?: ProviderWorkspaceOverride | null;
65
+ }
66
+
67
+ export interface ProviderRouteValidationResult {
68
+ status: "ready" | "not-configured";
69
+ providerId: ProviderId;
70
+ backendKind: ProviderBackendKind;
71
+ message?: string;
72
+ diagnostics?: Record<string, string | number | boolean | null>;
73
+ }
74
+
75
+ export interface ProviderChatRequest {
76
+ prompt: string;
77
+ route: ProviderRoute;
78
+ runtime: ResolvedRuntimeConfig;
79
+ workspaceRoot: string;
80
+ projectInstructions?: ProjectInstructions | null;
81
+ localConfig?: ProviderWorkspaceOverride | null;
82
+ }
83
+
84
+ export interface ProviderChatResponse {
85
+ text: string;
86
+ rawOutput?: string;
87
+ }
88
+
89
+ export interface ProviderRuntime {
90
+ providerId: ProviderId;
91
+ label: string;
92
+ modelPickerLabel?: string;
93
+ backendKind: ProviderBackendKind;
94
+ routeAvailable: boolean;
95
+ routeStatus: string;
96
+ routeSetupMessage?: string;
97
+ launchAvailable: boolean;
98
+ isRouteConfigured?: () => boolean;
99
+ validateRoute?: (request: ProviderRouteValidationRequest) => Promise<ProviderRouteValidationResult>;
100
+ discoverModels: () => ProviderModelDiscoveryResult;
101
+ refreshModels?: (options: { cwd: string; localConfig?: ProviderWorkspaceOverride | null }) => Promise<ProviderModelDiscoveryResult>;
102
+ run?: (request: ProviderChatRequest, handlers: BackendRunHandlers) => () => void;
103
+ }
@@ -0,0 +1,148 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { BackendProgressUpdate } from "./types.js";
4
+ import type { RunToolActivity } from "../../session/types.js";
5
+ import { createCodexJsonStreamParser } from "./codexJsonStream.js";
6
+
7
+ test("streams incremental agent_message growth as assistant deltas", () => {
8
+ const assistant: string[] = [];
9
+ const observedFinalAnswers: string[] = [];
10
+ const parser = createCodexJsonStreamParser({
11
+ onAssistantDelta: (chunk) => assistant.push(chunk),
12
+ onFinalAnswerObserved: (response) => observedFinalAnswers.push(response),
13
+ });
14
+
15
+ assert.equal(parser.feedLine(JSON.stringify({
16
+ type: "item.started",
17
+ item: { id: "msg-1", type: "agent_message", text: "Hello" },
18
+ })), true);
19
+ assert.equal(parser.feedLine(JSON.stringify({
20
+ type: "item.updated",
21
+ item: { id: "msg-1", type: "agent_message", text: "Hello world" },
22
+ })), true);
23
+ assert.equal(parser.feedLine(JSON.stringify({
24
+ type: "item.completed",
25
+ item: { id: "msg-1", type: "agent_message", text: "Hello world!" },
26
+ })), true);
27
+ assert.equal(parser.feedLine(JSON.stringify({
28
+ type: "turn.completed",
29
+ })), true);
30
+
31
+ assert.deepEqual(assistant, ["Hello", " world", "!"]);
32
+ assert.deepEqual(observedFinalAnswers, ["Hello world!"]);
33
+ assert.equal(parser.getFinalResponse(), "Hello world!");
34
+ });
35
+
36
+ test("surfaces command execution lifecycle as tool activity", () => {
37
+ const toolActivities: RunToolActivity[] = [];
38
+ const parser = createCodexJsonStreamParser({
39
+ onToolActivity: (activity) => toolActivities.push(activity),
40
+ });
41
+
42
+ parser.feedLine(JSON.stringify({
43
+ type: "item.started",
44
+ item: {
45
+ id: "cmd-1",
46
+ type: "command_execution",
47
+ command: "rg --files",
48
+ status: "in_progress",
49
+ aggregated_output: "",
50
+ },
51
+ }));
52
+ parser.feedLine(JSON.stringify({
53
+ type: "item.completed",
54
+ item: {
55
+ id: "cmd-1",
56
+ type: "command_execution",
57
+ command: "rg --files",
58
+ status: "completed",
59
+ aggregated_output: "src/app.tsx\nsrc/ui/Timeline.tsx\n",
60
+ exit_code: 0,
61
+ },
62
+ }));
63
+
64
+ assert.equal(toolActivities.length, 2);
65
+ assert.equal(toolActivities[0]?.status, "running");
66
+ assert.equal(toolActivities[0]?.command, "rg --files");
67
+ assert.equal(toolActivities[1]?.status, "completed");
68
+ assert.match(toolActivities[1]?.summary ?? "", /src\/app\.tsx/i);
69
+ });
70
+
71
+ test("emits progress updates for todo_list and reasoning items", () => {
72
+ const progress: BackendProgressUpdate[] = [];
73
+ const parser = createCodexJsonStreamParser({
74
+ onProgress: (update) => progress.push(update),
75
+ });
76
+
77
+ parser.feedLine(JSON.stringify({
78
+ type: "item.updated",
79
+ item: {
80
+ id: "todo-1",
81
+ type: "todo_list",
82
+ items: [
83
+ { text: "Inspect workspace", completed: true },
84
+ { text: "Write file", completed: false },
85
+ ],
86
+ },
87
+ }));
88
+ parser.feedLine(JSON.stringify({
89
+ type: "item.updated",
90
+ item: {
91
+ id: "reason-1",
92
+ type: "reasoning",
93
+ text: "Verifying the generated output\n\nChecking edge cases",
94
+ },
95
+ }));
96
+
97
+ assert.deepEqual(progress, [
98
+ { id: "todo-1", source: "todo", text: "Todo 1/2: Write file" },
99
+ { id: "reason-1", source: "reasoning", text: "Verifying the generated output\n\nChecking edge cases" },
100
+ ]);
101
+ });
102
+
103
+ test("reuses the same progress id when a structured update grows", () => {
104
+ const progress: BackendProgressUpdate[] = [];
105
+ const parser = createCodexJsonStreamParser({
106
+ onProgress: (update) => progress.push(update),
107
+ });
108
+
109
+ parser.feedLine(JSON.stringify({
110
+ type: "item.updated",
111
+ item: {
112
+ id: "reason-7",
113
+ type: "reasoning",
114
+ text: "Inspecting the config",
115
+ },
116
+ }));
117
+ parser.feedLine(JSON.stringify({
118
+ type: "item.updated",
119
+ item: {
120
+ id: "reason-7",
121
+ type: "reasoning",
122
+ text: "Inspecting the config\n\nComparing runtime defaults",
123
+ },
124
+ }));
125
+
126
+ assert.deepEqual(progress, [
127
+ { id: "reason-7", source: "reasoning", text: "Inspecting the config" },
128
+ { id: "reason-7", source: "reasoning", text: "Inspecting the config\n\nComparing runtime defaults" },
129
+ ]);
130
+ });
131
+
132
+ test("captures turn failures from structured events", () => {
133
+ const parser = createCodexJsonStreamParser({});
134
+
135
+ parser.feedLine(JSON.stringify({
136
+ type: "turn.failed",
137
+ error: { message: "Permission denied" },
138
+ }));
139
+
140
+ assert.equal(parser.getFailureMessage(), "Permission denied");
141
+ });
142
+
143
+ test("returns false for non-json lines so callers can fall back to transcript parsing", () => {
144
+ const parser = createCodexJsonStreamParser({});
145
+
146
+ assert.equal(parser.feedLine("assistant"), false);
147
+ assert.equal(parser.hasStructuredEvents(), false);
148
+ });
@@ -0,0 +1,305 @@
1
+ import type { BackendProgressUpdate } from "./types.js";
2
+ import type { RunToolActivity } from "../../session/types.js";
3
+
4
+ type CodexThreadEvent =
5
+ | { type: "thread.started"; thread_id: string }
6
+ | { type: "turn.started" }
7
+ | { type: "turn.completed"; usage?: { input_tokens?: number; cached_input_tokens?: number; output_tokens?: number } }
8
+ | { type: "turn.failed"; error?: { message?: string } }
9
+ | { type: "error"; message?: string }
10
+ | { type: "item.started" | "item.updated" | "item.completed"; item: CodexThreadItem };
11
+
12
+ type CodexThreadItem =
13
+ | {
14
+ id: string;
15
+ type: "agent_message";
16
+ text: string;
17
+ }
18
+ | {
19
+ id: string;
20
+ type: "reasoning";
21
+ text: string;
22
+ }
23
+ | {
24
+ id: string;
25
+ type: "command_execution";
26
+ command: string;
27
+ status: "in_progress" | "completed" | "failed";
28
+ aggregated_output?: string;
29
+ exit_code?: number;
30
+ }
31
+ | {
32
+ id: string;
33
+ type: "mcp_tool_call";
34
+ server: string;
35
+ tool: string;
36
+ status: "in_progress" | "completed" | "failed";
37
+ error?: { message?: string };
38
+ }
39
+ | {
40
+ id: string;
41
+ type: "web_search";
42
+ query: string;
43
+ }
44
+ | {
45
+ id: string;
46
+ type: "todo_list";
47
+ items: Array<{ text: string; completed: boolean }>;
48
+ }
49
+ | {
50
+ id: string;
51
+ type: "file_change";
52
+ changes: Array<{ path: string; kind: "add" | "delete" | "update" }>;
53
+ status: "completed" | "failed";
54
+ }
55
+ | {
56
+ id: string;
57
+ type: "error";
58
+ message: string;
59
+ };
60
+
61
+ export interface CodexJsonStreamHandlers {
62
+ onAssistantDelta?: (chunk: string) => void;
63
+ onFinalAnswerObserved?: (response: string) => void;
64
+ onProgress?: (update: BackendProgressUpdate) => void;
65
+ onToolActivity?: (activity: RunToolActivity) => void;
66
+ }
67
+
68
+ function normalizeProgressText(text: string | undefined): string | null {
69
+ if (!text) return null;
70
+ const normalized = text
71
+ .replace(/\r\n/g, "\n")
72
+ .replace(/\r/g, "\n")
73
+ .replace(/[ \t]+\n/g, "\n")
74
+ .trim();
75
+ return normalized || null;
76
+ }
77
+
78
+ function firstMeaningfulLine(text: string | undefined): string | null {
79
+ if (!text) return null;
80
+ const line = text
81
+ .replace(/\r\n/g, "\n")
82
+ .replace(/\r/g, "\n")
83
+ .split("\n")
84
+ .map((part) => part.trim())
85
+ .find(Boolean);
86
+ return line ?? null;
87
+ }
88
+
89
+ function summarizeCommandExecution(item: Extract<CodexThreadItem, { type: "command_execution" }>): string {
90
+ const firstLine = firstMeaningfulLine(item.aggregated_output);
91
+ if (item.status === "failed") {
92
+ if (firstLine) return firstLine;
93
+ if (item.exit_code != null) return `Exit code ${item.exit_code}`;
94
+ return "Failed";
95
+ }
96
+ if (firstLine) return firstLine;
97
+ if (item.exit_code != null) return `Exit code ${item.exit_code}`;
98
+ return item.status === "completed" ? "Completed" : "Running";
99
+ }
100
+
101
+ function summarizeTodoList(item: Extract<CodexThreadItem, { type: "todo_list" }>): string | null {
102
+ if (!item.items.length) return null;
103
+ const completedCount = item.items.filter((entry) => entry.completed).length;
104
+ const nextPending = item.items.find((entry) => !entry.completed);
105
+ if (nextPending) {
106
+ return `Todo ${completedCount}/${item.items.length}: ${nextPending.text}`;
107
+ }
108
+ return `Todo ${completedCount}/${item.items.length}: all tasks complete`;
109
+ }
110
+
111
+ function summarizeReasoning(item: Extract<CodexThreadItem, { type: "reasoning" }>): string | null {
112
+ return normalizeProgressText(item.text);
113
+ }
114
+
115
+ function summarizeFileChange(item: Extract<CodexThreadItem, { type: "file_change" }>): string | null {
116
+ if (!item.changes.length) return null;
117
+ const [first] = item.changes;
118
+ if (item.changes.length === 1 && first) {
119
+ const verb = first.kind === "add" ? "Created" : first.kind === "delete" ? "Deleted" : "Updated";
120
+ return `${verb} ${first.path}`;
121
+ }
122
+ return `Applied ${item.changes.length} file changes`;
123
+ }
124
+
125
+ function mapToolActivity(item: Extract<CodexThreadItem, {
126
+ type: "command_execution" | "mcp_tool_call" | "web_search";
127
+ }>, phase: "item.started" | "item.updated" | "item.completed", existing: RunToolActivity | undefined): RunToolActivity {
128
+ const startedAt = existing?.startedAt ?? Date.now();
129
+
130
+ if (item.type === "command_execution") {
131
+ const status = item.status === "in_progress" ? "running" : item.status;
132
+ return {
133
+ id: item.id,
134
+ command: item.command,
135
+ status,
136
+ startedAt,
137
+ completedAt: status === "running" ? null : Date.now(),
138
+ summary: status === "running" ? undefined : summarizeCommandExecution(item),
139
+ };
140
+ }
141
+
142
+ if (item.type === "mcp_tool_call") {
143
+ return {
144
+ id: item.id,
145
+ command: `${item.server}:${item.tool}`,
146
+ status: item.status === "in_progress" ? "running" : item.status,
147
+ startedAt,
148
+ completedAt: item.status === "in_progress" ? null : Date.now(),
149
+ summary: item.status === "failed"
150
+ ? item.error?.message ?? "Failed"
151
+ : item.status === "completed"
152
+ ? "Completed"
153
+ : undefined,
154
+ };
155
+ }
156
+
157
+ return {
158
+ id: item.id,
159
+ command: `web search: ${item.query}`,
160
+ status: phase === "item.completed" ? "completed" : "running",
161
+ startedAt,
162
+ completedAt: phase === "item.completed" ? Date.now() : null,
163
+ summary: phase === "item.completed" ? "Completed" : undefined,
164
+ };
165
+ }
166
+
167
+ export function createCodexJsonStreamParser(handlers: CodexJsonStreamHandlers) {
168
+ const assistantTextById = new Map<string, string>();
169
+ const progressTextById = new Map<string, string>();
170
+ const toolActivityById = new Map<string, RunToolActivity>();
171
+ let sawEvent = false;
172
+ let finalResponse = "";
173
+ let finalAnswerObserved = false;
174
+ let failureMessage: string | null = null;
175
+
176
+ const emitProgress = (
177
+ key: string,
178
+ source: BackendProgressUpdate["source"],
179
+ summary: string | null | undefined,
180
+ ) => {
181
+ const next = normalizeProgressText(summary ?? "");
182
+ if (!next) return;
183
+ if (progressTextById.get(key) === next) return;
184
+ progressTextById.set(key, next);
185
+ handlers.onProgress?.({ id: key, source, text: next });
186
+ };
187
+
188
+ // Codex JSON stream sends cumulative text per item ID rather than deltas.
189
+ // We track the last emitted text and emit only the newly appended suffix.
190
+ const emitAssistantText = (itemId: string, nextText: string) => {
191
+ const previous = assistantTextById.get(itemId) ?? "";
192
+ assistantTextById.set(itemId, nextText);
193
+ finalResponse = nextText;
194
+
195
+ if (!nextText) return;
196
+ if (!previous) {
197
+ handlers.onAssistantDelta?.(nextText);
198
+ return;
199
+ }
200
+
201
+ if (nextText.startsWith(previous)) {
202
+ const delta = nextText.slice(previous.length);
203
+ if (delta) {
204
+ handlers.onAssistantDelta?.(delta);
205
+ }
206
+ }
207
+ };
208
+
209
+ const upsertTool = (item: Extract<CodexThreadItem, {
210
+ type: "command_execution" | "mcp_tool_call" | "web_search";
211
+ }>, phase: "item.started" | "item.updated" | "item.completed") => {
212
+ const next = mapToolActivity(item, phase, toolActivityById.get(item.id));
213
+ toolActivityById.set(item.id, next);
214
+ handlers.onToolActivity?.(next);
215
+ };
216
+
217
+ const handleItem = (phase: "item.started" | "item.updated" | "item.completed", item: CodexThreadItem) => {
218
+ switch (item.type) {
219
+ case "agent_message":
220
+ emitAssistantText(item.id, item.text ?? "");
221
+ break;
222
+ case "reasoning":
223
+ emitProgress(item.id, "reasoning", summarizeReasoning(item));
224
+ break;
225
+ case "todo_list":
226
+ emitProgress(item.id, "todo", summarizeTodoList(item));
227
+ break;
228
+ case "command_execution":
229
+ upsertTool(item, phase);
230
+ emitProgress(item.id, "tool", item.status === "in_progress" ? `Running ${item.command}` : summarizeCommandExecution(item));
231
+ break;
232
+ case "mcp_tool_call":
233
+ upsertTool(item, phase);
234
+ emitProgress(
235
+ item.id,
236
+ "tool",
237
+ item.status === "in_progress" ? `Calling ${item.server}:${item.tool}` : item.error?.message ?? `Completed ${item.server}:${item.tool}`,
238
+ );
239
+ break;
240
+ case "web_search":
241
+ upsertTool(item, phase);
242
+ emitProgress(item.id, "tool", `Searching web: ${item.query}`);
243
+ break;
244
+ case "file_change":
245
+ emitProgress(item.id, "activity", summarizeFileChange(item));
246
+ break;
247
+ case "error":
248
+ failureMessage = item.message;
249
+ break;
250
+ }
251
+ };
252
+
253
+ return {
254
+ feedLine(line: string): boolean {
255
+ const trimmed = line.trim();
256
+ if (!trimmed) return false;
257
+
258
+ let event: CodexThreadEvent;
259
+ try {
260
+ event = JSON.parse(trimmed) as CodexThreadEvent;
261
+ } catch {
262
+ return false;
263
+ }
264
+
265
+ if (!event || typeof event !== "object" || typeof event.type !== "string") {
266
+ return false;
267
+ }
268
+
269
+ sawEvent = true;
270
+
271
+ switch (event.type) {
272
+ case "item.started":
273
+ case "item.updated":
274
+ case "item.completed":
275
+ handleItem(event.type, event.item);
276
+ break;
277
+ case "turn.completed":
278
+ if (!finalAnswerObserved && finalResponse) {
279
+ finalAnswerObserved = true;
280
+ handlers.onFinalAnswerObserved?.(finalResponse);
281
+ }
282
+ break;
283
+ case "turn.failed":
284
+ failureMessage = event.error?.message ?? "Turn failed";
285
+ break;
286
+ case "error":
287
+ failureMessage = event.message ?? "Stream error";
288
+ break;
289
+ default:
290
+ break;
291
+ }
292
+
293
+ return true;
294
+ },
295
+ getFinalResponse(): string {
296
+ return finalResponse;
297
+ },
298
+ getFailureMessage(): string | null {
299
+ return failureMessage;
300
+ },
301
+ hasStructuredEvents(): boolean {
302
+ return sawEvent;
303
+ },
304
+ };
305
+ }
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import test from "node:test";
5
+
6
+ test("codex subprocess attaches output handlers before writing stdin", () => {
7
+ const source = readFileSync(fileURLToPath(new URL("./codexSubprocess.ts", import.meta.url)), "utf8");
8
+ const spawnIndex = source.indexOf("proc = spawnCodexProcess");
9
+ const stdoutIndex = source.indexOf('proc.stdout?.on("data"', spawnIndex);
10
+ const stderrIndex = source.indexOf('proc.stderr?.on("data"', spawnIndex);
11
+ const closeIndex = source.indexOf('proc.on("close"', spawnIndex);
12
+ const errorIndex = source.indexOf('proc.on("error"', spawnIndex);
13
+ const stdinWriteIndex = source.indexOf("proc.stdin?.write", spawnIndex);
14
+
15
+ assert.notEqual(spawnIndex, -1);
16
+ assert.notEqual(stdoutIndex, -1);
17
+ assert.notEqual(stderrIndex, -1);
18
+ assert.notEqual(closeIndex, -1);
19
+ assert.notEqual(errorIndex, -1);
20
+ assert.notEqual(stdinWriteIndex, -1);
21
+ assert.ok(stdoutIndex < stdinWriteIndex);
22
+ assert.ok(stderrIndex < stdinWriteIndex);
23
+ assert.ok(closeIndex < stdinWriteIndex);
24
+ assert.ok(errorIndex < stdinWriteIndex);
25
+ });
26
+
27
+ test("codex subprocess supports raw prompt passthrough before wrapped prompt fallback", () => {
28
+ const source = readFileSync(fileURLToPath(new URL("./codexSubprocess.ts", import.meta.url)), "utf8");
29
+
30
+ assert.match(source, /const promptPolicy = options\.promptPolicy \?\? "wrapped"/);
31
+ assert.match(source, /promptPolicy === "raw"\s+\?\s+prompt\s+:\s+buildCodexPrompt/s);
32
+ });
33
+
34
+ test("codex subprocess cleanup skips kill after process close", () => {
35
+ const source = readFileSync(fileURLToPath(new URL("./codexSubprocess.ts", import.meta.url)), "utf8");
36
+ const closeIndex = source.indexOf('proc.on("close"', source.indexOf("proc = spawnCodexProcess"));
37
+ const exitedIndex = source.indexOf("procExited = true", closeIndex);
38
+ const cleanupIndex = source.indexOf("return () =>", exitedIndex);
39
+ const skipIndex = source.indexOf("!proc || procExited || proc.killed", cleanupIndex);
40
+ const killIndex = source.indexOf("proc.kill()", cleanupIndex);
41
+
42
+ assert.notEqual(closeIndex, -1);
43
+ assert.notEqual(exitedIndex, -1);
44
+ assert.notEqual(cleanupIndex, -1);
45
+ assert.notEqual(skipIndex, -1);
46
+ assert.notEqual(killIndex, -1);
47
+ assert.ok(exitedIndex < cleanupIndex);
48
+ assert.ok(skipIndex < killIndex);
49
+ });
50
+
51
+ test("codex subprocess reports lifecycle boundaries for terminal title reassertion", () => {
52
+ const source = readFileSync(fileURLToPath(new URL("./codexSubprocess.ts", import.meta.url)), "utf8");
53
+ const beforeSpawnIndex = source.indexOf('handlers.onProcessLifecycle?.("before-spawn")');
54
+ const spawnIndex = source.indexOf("proc = spawnCodexProcess");
55
+ const spawnedIndex = source.indexOf('handlers.onProcessLifecycle?.("spawned")', spawnIndex);
56
+ const closeIndex = source.indexOf('proc.on("close"', spawnIndex);
57
+ const exitIndex = source.indexOf('handlers.onProcessLifecycle?.("exit")', closeIndex);
58
+ const errorIndex = source.indexOf('proc.on("error"', spawnIndex);
59
+ const lifecycleErrorIndex = source.indexOf('handlers.onProcessLifecycle?.("error")', errorIndex);
60
+ const cleanupIndex = source.indexOf("return () =>", spawnedIndex);
61
+ const lifecycleCleanupIndex = source.indexOf('handlers.onProcessLifecycle?.("cleanup")', cleanupIndex);
62
+
63
+ assert.ok(beforeSpawnIndex >= 0 && beforeSpawnIndex < spawnIndex);
64
+ assert.ok(spawnedIndex > spawnIndex);
65
+ assert.ok(exitIndex > closeIndex);
66
+ assert.ok(lifecycleErrorIndex > errorIndex);
67
+ assert.ok(lifecycleCleanupIndex > cleanupIndex);
68
+ });