@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,695 @@
1
+ import type { RunToolActivity } from "../../session/types.js";
2
+
3
+ const ANSI_ESCAPE_PATTERN =
4
+ // Strip ANSI color/control sequences before attempting transcript parsing.
5
+ /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
6
+
7
+ const NOISE_EXACT_LINES = new Set([
8
+ "reading additional input from stdin...",
9
+ "tokens used",
10
+ "user",
11
+ "codex",
12
+ "assistant",
13
+ // Injected system-prompt lines (from buildCodexPrompt) that the backend echoes back
14
+ "the user request below is the task to handle now.",
15
+ "do not reply with generic readiness or ask what they want changed if the request is already specific.",
16
+ "runtime permissions are read-only for this turn.",
17
+ "inspect files and answer carefully, but do not claim to have edited files unless you actually could.",
18
+ "the current permissions allow workspace edits, but this turn is still in suggest mode.",
19
+ "inspect the repo and answer carefully without making file changes in this turn.",
20
+ "if the request is actionable, make the change in the workspace before responding.",
21
+ "you are running inside the user's current workspace with write access.",
22
+ "only ask a follow-up question if a required detail is truly missing and blocks the work.",
23
+ "default to best-effort continuation instead of stopping for clarification.",
24
+ "if a detail is missing but non-critical, make the most reasonable assumption and state it briefly.",
25
+ "if multiple paths are possible, choose one sensible path and continue.",
26
+ "only ask a blocking follow-up question if proceeding would likely use the wrong file, wrong command, destructive behavior, or produce fundamentally incorrect output.",
27
+ "if you are truly blocked on one critical missing fact, end the response with exactly one line in this format: [question]: <your question>",
28
+ "after doing the work, summarize what changed.",
29
+ "you are in read-only mode, so inspect files and answer carefully, but do not claim to have edited files unless you actually could.",
30
+ "task:",
31
+ // Section headers that may leak from internal prompt scaffolding
32
+ "analysis",
33
+ "analysis:",
34
+ "suggestions",
35
+ "suggestions:",
36
+ "summary",
37
+ "summary:",
38
+ "no code changes are needed",
39
+ "no code changes are needed.",
40
+ ]);
41
+
42
+ const NOISE_PREFIXES = [
43
+ "OpenAI Codex v",
44
+ "workdir:",
45
+ "model:",
46
+ "provider:",
47
+ "approval:",
48
+ "sandbox:",
49
+ "reasoning effort:",
50
+ "reasoning summaries:",
51
+ "session id:",
52
+ "auth:",
53
+ "tokens used", // catches "tokens used", "tokens used:", "tokens used1,969", etc.
54
+ "act with strong autonomy",
55
+ "act like a coding agent",
56
+ // Additional internal scaffolding patterns
57
+ "use these markers to structure",
58
+ "hidden workflow",
59
+ "internal rubric",
60
+ "prompt-management",
61
+ "tool-selection rationale",
62
+ ];
63
+
64
+ export function stripAnsi(text: string): string {
65
+ return text.replace(ANSI_ESCAPE_PATTERN, "");
66
+ }
67
+
68
+ const STDERR_NOISE_PATTERNS = [
69
+ /\[\d+\/\d+\]/, // Progress fractions: [3/10]
70
+ /^\s*\d+\/\d+\s*$/, // Bare fractions: 3/10
71
+ /\d+(\.\d+)?%/, // Percentages: 45.2%
72
+ /^[\s#=.]+$/, // Progress bars: ####, ====, ....
73
+ /^[\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\-\\|/]+$/, // Spinner characters
74
+ /^\(node:\d+\)/, // Node.js warnings: (node:1234)
75
+ /DeprecationWarning:/i, // Node deprecation warnings
76
+ /ExperimentalWarning:/i, // Node experimental warnings
77
+ /^\s*Warning:/i, // Generic warnings
78
+ ];
79
+
80
+ export function isStderrNoise(line: string): boolean {
81
+ if (isNoiseLine(line)) return true;
82
+ const trimmed = line.trim();
83
+ if (!trimmed) return true;
84
+ return STDERR_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
85
+ }
86
+
87
+ // Keep line breaks but drop control bytes that can move or corrupt terminal cursor/layout.
88
+ export function stripNonPrintableControls(text: string): string {
89
+ return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
90
+ }
91
+
92
+ // Box-drawing and block-element Unicode characters (U+2500–U+259F) used by
93
+ // terminal TUI frame decorations — strip these from streaming output.
94
+ const BOX_DRAWING_BLOCK_ELEMENTS = /[\u2500-\u259F]/g;
95
+
96
+ /**
97
+ * Stateful factory that pre-sanitizes raw stdout chunks before the stream
98
+ * parser sees them. Maintains a carryover buffer so that ANSI escape
99
+ * sequences split across Node `data` events are handled correctly.
100
+ */
101
+ export function createStdoutSanitizer(): {
102
+ process(chunk: string): string;
103
+ flush(): string;
104
+ } {
105
+ // Lazy-import to avoid circular deps at module parse time.
106
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
107
+ const { sanitizeTerminalOutput } = require("../terminal/terminalSanitize.js") as typeof import("../terminal/terminalSanitize.js");
108
+
109
+ let carryover = "";
110
+
111
+ const sanitize = (text: string): string => {
112
+ let cleaned = sanitizeTerminalOutput(text);
113
+ cleaned = cleaned.replace(BOX_DRAWING_BLOCK_ELEMENTS, "");
114
+ return cleaned;
115
+ };
116
+
117
+ return {
118
+ process(chunk: string): string {
119
+ const input = carryover + chunk;
120
+ carryover = "";
121
+
122
+ // Scan the last 20 characters for an incomplete ESC sequence.
123
+ // An ESC (0x1B) not followed by a complete CSI/OSC/Fe terminator
124
+ // means the sequence spans into the next chunk.
125
+ const tail = input.slice(-20);
126
+ const lastEsc = tail.lastIndexOf("\u001B");
127
+ if (lastEsc !== -1) {
128
+ const afterEsc = tail.slice(lastEsc);
129
+ // Check if this looks like a complete sequence:
130
+ // - Fe: ESC + single byte in @-Z\_
131
+ // - CSI: ESC [ ... <letter>
132
+ // - OSC: ESC ] ... (BEL or ST)
133
+ const isComplete =
134
+ /^\u001B[@-Z\\-_]/.test(afterEsc) || // Fe
135
+ /^\u001B\[[0-?]*[ -/]*[@-~]/.test(afterEsc) || // CSI complete
136
+ /^\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/.test(afterEsc); // OSC complete
137
+
138
+ if (!isComplete && afterEsc.length < 20) {
139
+ // Incomplete escape — hold it back for the next chunk
140
+ const splitAt = input.length - tail.length + lastEsc;
141
+ carryover = input.slice(splitAt);
142
+ return sanitize(input.slice(0, splitAt));
143
+ }
144
+ }
145
+
146
+ return sanitize(input);
147
+ },
148
+
149
+ flush(): string {
150
+ if (!carryover) return "";
151
+ const remaining = carryover;
152
+ carryover = "";
153
+ return sanitize(remaining);
154
+ },
155
+ };
156
+ }
157
+
158
+ type TranscriptSection = "preamble" | "task" | "user" | "assistant" | "tool_output" | "postlude";
159
+
160
+ export interface CodexTranscriptStreamHandlers {
161
+ onThinkingLine?: (line: string) => void;
162
+ onAssistantDelta?: (chunk: string) => void;
163
+ onToolActivity?: (activity: RunToolActivity) => void;
164
+ }
165
+
166
+ export function normalizeLines(raw: string): string[] {
167
+ return stripNonPrintableControls(stripAnsi(raw))
168
+ .replace(/\r\n/g, "\n")
169
+ .replace(/\r/g, "\n")
170
+ .split("\n")
171
+ .map((line) => line.replace(/\s+$/g, ""));
172
+ }
173
+
174
+ function isDivider(line: string): boolean {
175
+ const trimmed = line.trim();
176
+ return trimmed.length > 0 && /^-+$/.test(trimmed);
177
+ }
178
+
179
+ export function isNoiseLine(line: string): boolean {
180
+ const trimmed = line.trim();
181
+ if (!trimmed) return false;
182
+ if (isDivider(trimmed)) return true;
183
+ if (NOISE_EXACT_LINES.has(trimmed.toLowerCase())) return true;
184
+
185
+ const lower = trimmed.toLowerCase();
186
+ return NOISE_PREFIXES.some((prefix) => lower.startsWith(prefix.toLowerCase()));
187
+ }
188
+
189
+ function isAssistantLabel(line: string): boolean {
190
+ const lower = line.trim().toLowerCase();
191
+ return lower === "assistant" || lower === "codex";
192
+ }
193
+
194
+ // Detect lines that mark the start of tool/shell execution output from the
195
+ // codex backend. When the backend runs commands (Get-ChildItem, rg, etc.) it
196
+ // often emits markers like "$ cmd", "> cmd", "tool_call: shell", or fenced
197
+ // code blocks tagged with "ex"/"shell"/"output". These should be hidden from
198
+ // the user and routed to the thinking stream instead.
199
+ const TOOL_EXEC_PATTERNS = [
200
+ /^\s*\$\s+\S/, // $ Get-ChildItem, $ rg --files …
201
+ /^\s*>\s+\S/, // > Get-ChildItem (PowerShell prompt)
202
+ /^\s*tool_call\s*:/i, // tool_call: shell
203
+ /^\s*```\s*(ex|shell|bash|powershell|output|cmd)\s*$/i, // fenced execution block
204
+ /^\s*\[running:?\s/i, // [running: Get-ChildItem]
205
+ /^\s*\[exec(uting)?:?\s/i, // [exec: rg --files]
206
+ /^\s*executing\s*:/i, // executing: Get-ChildItem
207
+ /^\s*reading\s+(file|from)\s+/i, // reading file src/...
208
+ /^\s*scanning\s+/i, // scanning directory...
209
+ /^\s*searching\s+/i, // searching for...
210
+ /^\s*writing\s+(to\s+)?file\s+/i, // writing to file...
211
+ /^\s*creating\s+file\s+/i, // creating file...
212
+ /^\s*deleting\s+file\s+/i, // deleting file...
213
+ /^\s*\[tool:\s/i, // [tool: read_file]
214
+ /^\s*\[function:\s/i, // [function: search]
215
+ /^\s*tool_use\s*:/i, // tool_use: read
216
+ /^\s*function_call\s*:/i, // function_call: write
217
+ ];
218
+
219
+ function isToolExecStart(line: string): boolean {
220
+ return TOOL_EXEC_PATTERNS.some((pattern) => pattern.test(line));
221
+ }
222
+
223
+ function isToolExecEnd(line: string): boolean {
224
+ const trimmed = line.trim();
225
+ // A closing fence (```) or a blank line after tool output ends the block
226
+ return trimmed === "```" || trimmed === "";
227
+ }
228
+
229
+ function shouldIgnoreStreamingLine(line: string): boolean {
230
+ const trimmed = line.trim();
231
+ if (!trimmed) return false;
232
+ if (isDivider(trimmed)) return true;
233
+ if (trimmed.toLowerCase() === "tokens used") return true;
234
+ if (isNoiseLine(trimmed)) return true;
235
+ return false;
236
+ }
237
+
238
+ function pluralize(count: number, singular: string, plural = `${singular}s`): string {
239
+ return `${count} ${count === 1 ? singular : plural}`;
240
+ }
241
+
242
+ function looksLikePath(line: string): boolean {
243
+ return /[\\/]/.test(line) || /\.[a-z0-9_-]+$/i.test(line);
244
+ }
245
+
246
+ function summarizeToolOutput(command: string, outputLines: string[]): {
247
+ status: RunToolActivity["status"];
248
+ summary: string;
249
+ } {
250
+ const lines = outputLines.map((line) => line.trim()).filter(Boolean);
251
+ if (lines.length === 0) {
252
+ return { status: "completed", summary: "Completed with no output" };
253
+ }
254
+
255
+ const firstLine = lines[0]!;
256
+ const lowerCommand = command.toLowerCase();
257
+ const failurePattern = /(error|exception|fatal|failed|permission denied|not recognized|not found)/i;
258
+ if (failurePattern.test(firstLine)) {
259
+ return { status: "failed", summary: firstLine };
260
+ }
261
+
262
+ if (/\brg\b/.test(lowerCommand) && /--files\b/.test(lowerCommand)) {
263
+ return { status: "completed", summary: `Found ${pluralize(lines.length, "file")}` };
264
+ }
265
+
266
+ if (/\b(get-childitem|ls|dir)\b/.test(lowerCommand)) {
267
+ const noun = lines.every(looksLikePath) ? "item" : "line";
268
+ return { status: "completed", summary: `Listed ${pluralize(lines.length, noun)}` };
269
+ }
270
+
271
+ if (/\b(rg|grep|select-string|findstr)\b/.test(lowerCommand)) {
272
+ return { status: "completed", summary: `Found ${pluralize(lines.length, "match", "matches")}` };
273
+ }
274
+
275
+ if (lines.length === 1) {
276
+ return { status: "completed", summary: firstLine };
277
+ }
278
+
279
+ if (lines.every(looksLikePath)) {
280
+ return { status: "completed", summary: `Returned ${pluralize(lines.length, "path")}` };
281
+ }
282
+
283
+ return { status: "completed", summary: `Produced ${pluralize(lines.length, "line")} of output` };
284
+ }
285
+
286
+ export function createCodexTranscriptStreamParser(handlers: CodexTranscriptStreamHandlers) {
287
+ let pending = "";
288
+ let section: TranscriptSection = "preamble";
289
+ let emittedAssistantLine = false;
290
+ let toolCounter = 0;
291
+ let activeTool: {
292
+ id: string;
293
+ command: string;
294
+ startedAt: number;
295
+ outputLines: string[];
296
+ } | null = null;
297
+
298
+ // Partial-line flush: emit pending content after a short delay if we're in
299
+ // the assistant section, so sub-line text appears progressively.
300
+ let partialFlushTimer: ReturnType<typeof setTimeout> | null = null;
301
+ let pendingEmittedAsPartial = false;
302
+
303
+ // Code fence buffering: buffer lines inside fenced blocks and emit them
304
+ // atomically when the closing fence arrives (or after a safety timeout).
305
+ let inCodeFence = false;
306
+ let codeFenceBuffer: string[] = [];
307
+ let codeFenceTimeout: ReturnType<typeof setTimeout> | null = null;
308
+ const CODE_FENCE_TIMEOUT_MS = 3000;
309
+
310
+ const clearPartialFlushTimer = () => {
311
+ if (partialFlushTimer) {
312
+ clearTimeout(partialFlushTimer);
313
+ partialFlushTimer = null;
314
+ }
315
+ };
316
+
317
+ const clearCodeFenceTimeout = () => {
318
+ if (codeFenceTimeout) {
319
+ clearTimeout(codeFenceTimeout);
320
+ codeFenceTimeout = null;
321
+ }
322
+ };
323
+
324
+ const emitThinking = (line: string) => {
325
+ const cleaned = line.replace(/\s+$/g, "");
326
+ if (!cleaned.trim()) return;
327
+ handlers.onThinkingLine?.(cleaned);
328
+ };
329
+
330
+ const emitAssistant = (line: string) => {
331
+ const cleaned = line.replace(/\s+$/g, "");
332
+ if (!cleaned && !emittedAssistantLine) return;
333
+
334
+ // Code fence buffering: accumulate fenced lines and emit atomically
335
+ if (!inCodeFence && /^\s*```/.test(cleaned)) {
336
+ inCodeFence = true;
337
+ codeFenceBuffer = [cleaned];
338
+ clearCodeFenceTimeout();
339
+ codeFenceTimeout = setTimeout(() => {
340
+ // Safety timeout: force-emit buffered content if fence never closes
341
+ if (inCodeFence && codeFenceBuffer.length > 0) {
342
+ const block = codeFenceBuffer.join("\n");
343
+ const chunk = emittedAssistantLine ? `\n${block}` : block;
344
+ handlers.onAssistantDelta?.(chunk);
345
+ emittedAssistantLine = true;
346
+ codeFenceBuffer = [];
347
+ inCodeFence = false;
348
+ }
349
+ codeFenceTimeout = null;
350
+ }, CODE_FENCE_TIMEOUT_MS);
351
+ return;
352
+ }
353
+
354
+ if (inCodeFence) {
355
+ codeFenceBuffer.push(cleaned);
356
+ // Detect closing fence
357
+ if (/^\s*```\s*$/.test(cleaned) && codeFenceBuffer.length > 1) {
358
+ clearCodeFenceTimeout();
359
+ const block = codeFenceBuffer.join("\n");
360
+ const chunk = emittedAssistantLine ? `\n${block}` : block;
361
+ handlers.onAssistantDelta?.(chunk);
362
+ emittedAssistantLine = true;
363
+ codeFenceBuffer = [];
364
+ inCodeFence = false;
365
+ }
366
+ return;
367
+ }
368
+
369
+ const chunk = emittedAssistantLine ? `\n${cleaned}` : cleaned;
370
+ handlers.onAssistantDelta?.(chunk);
371
+ emittedAssistantLine = true;
372
+ };
373
+
374
+ const finalizeActiveTool = () => {
375
+ if (!activeTool) return;
376
+
377
+ const result = summarizeToolOutput(activeTool.command, activeTool.outputLines);
378
+ handlers.onToolActivity?.({
379
+ id: activeTool.id,
380
+ command: activeTool.command,
381
+ status: result.status,
382
+ startedAt: activeTool.startedAt,
383
+ completedAt: Date.now(),
384
+ summary: result.summary,
385
+ });
386
+ activeTool = null;
387
+ };
388
+
389
+ const startToolCapture = (rawCommand: string) => {
390
+ finalizeActiveTool();
391
+ toolCounter += 1;
392
+ activeTool = {
393
+ id: `tool-${toolCounter}`,
394
+ command: rawCommand,
395
+ startedAt: Date.now(),
396
+ outputLines: [],
397
+ };
398
+ handlers.onToolActivity?.({
399
+ id: activeTool.id,
400
+ command: activeTool.command,
401
+ status: "running",
402
+ startedAt: activeTool.startedAt,
403
+ });
404
+ };
405
+
406
+ const processLine = (rawLine: string) => {
407
+ const line = stripNonPrintableControls(stripAnsi(rawLine)).replace(/\r/g, "");
408
+ const trimmed = line.trim();
409
+ const lower = trimmed.toLowerCase();
410
+
411
+ if (section === "postlude") {
412
+ return;
413
+ }
414
+
415
+ if (lower === "tokens used") {
416
+ section = "postlude";
417
+ return;
418
+ }
419
+
420
+ if (lower === "task:") {
421
+ section = "task";
422
+ return;
423
+ }
424
+
425
+ if (isAssistantLabel(line)) {
426
+ section = "assistant";
427
+ return;
428
+ }
429
+
430
+ if (lower === "user") {
431
+ section = "user";
432
+ return;
433
+ }
434
+
435
+ if (shouldIgnoreStreamingLine(line)) {
436
+ return;
437
+ }
438
+
439
+ if (section === "task") {
440
+ if (!trimmed) {
441
+ section = "preamble";
442
+ }
443
+ return;
444
+ }
445
+
446
+ // When inside a tool_output block, swallow lines until the block ends,
447
+ // then resume the assistant section.
448
+ if (section === "tool_output") {
449
+ if (isToolExecEnd(line)) {
450
+ finalizeActiveTool();
451
+ section = "assistant";
452
+ return;
453
+ }
454
+ if (activeTool && trimmed) {
455
+ activeTool.outputLines.push(line);
456
+ }
457
+ return;
458
+ }
459
+
460
+ if (section === "assistant") {
461
+ // Detect start of an inline tool execution block and switch to
462
+ // tool_output mode so the raw stdout is hidden from the user.
463
+ if (isToolExecStart(line)) {
464
+ const cmd = trimmed
465
+ .replace(/^\s*[$>]\s+/, "")
466
+ .replace(/^\s*tool_call\s*:\s*/i, "")
467
+ .replace(/^\s*```\s*/i, "")
468
+ .replace(/^\s*\[(running|exec|executing):?\s*/i, "")
469
+ .replace(/\]\s*$/, "");
470
+ if (cmd) startToolCapture(cmd);
471
+ section = "tool_output";
472
+ return;
473
+ }
474
+
475
+ emitAssistant(line);
476
+ return;
477
+ }
478
+
479
+ if (!trimmed) {
480
+ return;
481
+ }
482
+
483
+ // Auto-promotion: if we're in preamble and the line looks like substantive
484
+ // prose (multiple words, length > 40, no noise patterns, not a progress/status
485
+ // line), auto-promote to assistant section. This handles backends that skip
486
+ // the "Assistant:" label.
487
+ if (
488
+ section === "preamble"
489
+ && trimmed.length > 40
490
+ && (trimmed.match(/\s+/g)?.length ?? 0) >= 4
491
+ && !isNoiseLine(trimmed)
492
+ && !isToolExecStart(line)
493
+ && !/^(checking|scanning|searching|reading|loading|processing|analyzing|looking)\s/i.test(trimmed)
494
+ ) {
495
+ section = "assistant";
496
+ emitAssistant(line);
497
+ return;
498
+ }
499
+
500
+ emitThinking(line);
501
+ };
502
+
503
+ const feed = (chunk: string) => {
504
+ // If the previous pending content was already emitted as a partial,
505
+ // clear that flag since we're about to process new data.
506
+ if (pendingEmittedAsPartial) {
507
+ pendingEmittedAsPartial = false;
508
+ }
509
+
510
+ pending += chunk;
511
+ const normalized = pending.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
512
+ const lines = normalized.split("\n");
513
+ pending = lines.pop() ?? "";
514
+
515
+ for (const line of lines) {
516
+ processLine(line);
517
+ }
518
+
519
+ // Partial-line flush: if there's leftover content in pending and we're
520
+ // in the assistant section, schedule a timer to emit it as a partial delta.
521
+ clearPartialFlushTimer();
522
+ if (pending && section === "assistant" && !inCodeFence) {
523
+ partialFlushTimer = setTimeout(() => {
524
+ partialFlushTimer = null;
525
+ if (pending && section === "assistant" && !inCodeFence) {
526
+ const cleaned = stripNonPrintableControls(stripAnsi(pending)).replace(/\s+$/g, "");
527
+ if (cleaned) {
528
+ const partialChunk = emittedAssistantLine ? `\n${cleaned}` : cleaned;
529
+ handlers.onAssistantDelta?.(partialChunk);
530
+ emittedAssistantLine = true;
531
+ pendingEmittedAsPartial = true;
532
+ }
533
+ }
534
+ }, 100);
535
+ }
536
+ };
537
+
538
+ const flush = () => {
539
+ clearPartialFlushTimer();
540
+ clearCodeFenceTimeout();
541
+
542
+ // Force-emit any buffered code fence content
543
+ if (inCodeFence && codeFenceBuffer.length > 0) {
544
+ const block = codeFenceBuffer.join("\n");
545
+ const chunk = emittedAssistantLine ? `\n${block}` : block;
546
+ handlers.onAssistantDelta?.(chunk);
547
+ emittedAssistantLine = true;
548
+ codeFenceBuffer = [];
549
+ inCodeFence = false;
550
+ }
551
+
552
+ if (pending) {
553
+ // If pending was already emitted as a partial, skip re-emission
554
+ if (!pendingEmittedAsPartial) {
555
+ processLine(pending);
556
+ }
557
+ pending = "";
558
+ pendingEmittedAsPartial = false;
559
+ }
560
+ finalizeActiveTool();
561
+ };
562
+
563
+ return { feed, flush };
564
+ }
565
+
566
+ function trimBlankEdges(lines: string[]): string[] {
567
+ let start = 0;
568
+ let end = lines.length;
569
+
570
+ while (start < end && lines[start]?.trim() === "") start += 1;
571
+ while (end > start && lines[end - 1]?.trim() === "") end -= 1;
572
+
573
+ return lines.slice(start, end);
574
+ }
575
+
576
+ function stripToolOutputBlocks(lines: string[]): string[] {
577
+ const visible: string[] = [];
578
+ let inToolOutput = false;
579
+
580
+ for (const line of lines) {
581
+ if (inToolOutput) {
582
+ if (isToolExecEnd(line)) {
583
+ inToolOutput = false;
584
+ }
585
+ continue;
586
+ }
587
+
588
+ if (isToolExecStart(line)) {
589
+ inToolOutput = true;
590
+ continue;
591
+ }
592
+
593
+ visible.push(line);
594
+ }
595
+
596
+ return visible;
597
+ }
598
+
599
+ function dedupeTrailingRepeat(lines: string[]): string[] {
600
+ if (lines.length < 2) return lines;
601
+
602
+ const deduped: string[] = [];
603
+ for (const line of lines) {
604
+ if (deduped[deduped.length - 1] === line && line.trim()) continue;
605
+ deduped.push(line);
606
+ }
607
+ return deduped;
608
+ }
609
+
610
+ function extractLabeledAssistantBlock(lines: string[]): string[] {
611
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
612
+ const trimmed = lines[index]?.trim().toLowerCase();
613
+ if (trimmed !== "codex" && trimmed !== "assistant") continue;
614
+
615
+ const block: string[] = [];
616
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
617
+ const line = lines[cursor] ?? "";
618
+ const normalized = line.trim().toLowerCase();
619
+ if (normalized === "user" || normalized === "codex" || normalized === "assistant") break;
620
+ if (normalized.startsWith("tokens used")) break;
621
+ if (isDivider(line)) continue;
622
+ if (NOISE_PREFIXES.some((prefix) => normalized.startsWith(prefix.toLowerCase()))) break;
623
+ block.push(line);
624
+ }
625
+
626
+ const cleaned = trimBlankEdges(stripToolOutputBlocks(block));
627
+ if (cleaned.length > 0) {
628
+ return dedupeTrailingRepeat(cleaned);
629
+ }
630
+ }
631
+
632
+ return [];
633
+ }
634
+
635
+ // Handles backends that echo the full injected prompt (Task: + user message) before
636
+ // their response, without an "assistant" label. Finds the last "Task:" line, skips
637
+ // past the user's prompt text that follows it, then treats everything after as the
638
+ // actual response.
639
+ function extractAfterTaskSection(lines: string[]): string[] {
640
+ let taskIdx = -1;
641
+ for (let k = lines.length - 1; k >= 0; k--) {
642
+ if (lines[k]!.trim().toLowerCase() === "task:") {
643
+ taskIdx = k;
644
+ break;
645
+ }
646
+ }
647
+ if (taskIdx === -1) return [];
648
+
649
+ // Skip the "Task:" line and the user-prompt lines that follow (until blank or noise)
650
+ let i = taskIdx + 1;
651
+ while (i < lines.length && lines[i]!.trim() !== "") i++;
652
+ // Skip any blank separator lines
653
+ while (i < lines.length && lines[i]!.trim() === "") i++;
654
+
655
+ const rest = lines.slice(i);
656
+ const filtered = trimBlankEdges(
657
+ stripToolOutputBlocks(rest).filter((line) => {
658
+ if (!line.trim()) return true;
659
+ return !isNoiseLine(line);
660
+ }),
661
+ );
662
+
663
+ return dedupeTrailingRepeat(filtered);
664
+ }
665
+
666
+ export function sanitizeCodexTranscript(raw: string): string {
667
+ const lines = normalizeLines(raw);
668
+
669
+ // 1. Prefer a properly labeled "assistant:" / "codex:" block
670
+ const labeled = extractLabeledAssistantBlock(lines);
671
+ if (labeled.length > 0) {
672
+ return labeled.join("\n").trim();
673
+ }
674
+
675
+ // 2. If the backend echoed the full prompt (Task: … user message …) without a label,
676
+ // extract everything that comes after the task section.
677
+ const afterTask = extractAfterTaskSection(lines);
678
+ if (afterTask.length > 0) {
679
+ return afterTask.join("\n").trim();
680
+ }
681
+
682
+ // 3. Last resort: strip all known noise and return what's left.
683
+ const filtered = trimBlankEdges(
684
+ stripToolOutputBlocks(lines).filter((line) => {
685
+ if (!line.trim()) return true;
686
+ return !isNoiseLine(line);
687
+ }),
688
+ );
689
+
690
+ if (filtered.length === 0) {
691
+ return "Codex completed successfully, but no assistant response text was detected.";
692
+ }
693
+
694
+ return dedupeTrailingRepeat(filtered).join("\n").trim();
695
+ }
@@ -0,0 +1,13 @@
1
+ import { AVAILABLE_MODELS } from "../../config/settings.js";
2
+ import type { BackendProvider } from "./types.js";
3
+
4
+ export const openaiNativeProvider: BackendProvider = {
5
+ id: "openai-native",
6
+ label: "OpenAI Native",
7
+ description: "Planned native backend. Requires official API credentials when implemented.",
8
+ authState: "coming-soon",
9
+ authLabel: "Not implemented yet",
10
+ statusMessage:
11
+ "ChatGPT subscriptions and API billing are separate. Native execution is intentionally disabled in v1.",
12
+ supportsModels: (model) => (AVAILABLE_MODELS as readonly string[]).includes(model),
13
+ };