@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.
- package/README.md +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { formatCodexLaunchError, spawnCodexProcess } from "../executables/codexExecutable.js";
|
|
3
|
+
import { prepareCodexExecLaunch } from "../codexLaunch.js";
|
|
4
|
+
import * as perf from "../perf/profiler.js";
|
|
5
|
+
import { buildCodexPrompt } from "../codexPrompt.js";
|
|
6
|
+
import { createTerminalTitleSequenceStripper } from "../terminal/terminalTitle.js";
|
|
7
|
+
import { createCodexJsonStreamParser } from "./codexJsonStream.js";
|
|
8
|
+
import {
|
|
9
|
+
createCodexTranscriptStreamParser,
|
|
10
|
+
createStdoutSanitizer,
|
|
11
|
+
isStderrNoise,
|
|
12
|
+
sanitizeCodexTranscript,
|
|
13
|
+
stripAnsi,
|
|
14
|
+
stripNonPrintableControls,
|
|
15
|
+
} from "./codexTranscript.js";
|
|
16
|
+
import type { BackendProvider } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// Detects CLI error messages that indicate --experimental-json is not supported.
|
|
19
|
+
// When this fires the provider retries in legacy transcript mode.
|
|
20
|
+
function looksLikeUnsupportedStructuredOutput(raw: string): boolean {
|
|
21
|
+
return /experimental-json|unknown option|unrecognized option|unexpected argument|unexpected option/i.test(raw);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isProcessTerminationNoise(line: string): boolean {
|
|
25
|
+
return /^SUCCESS: The process with PID \d+ .* has been terminated\.$/.test(line.trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const codexSubprocessProvider: BackendProvider = {
|
|
29
|
+
id: "codex-subprocess",
|
|
30
|
+
label: "Codexa",
|
|
31
|
+
description: "Direct connection to the Codex neural network.",
|
|
32
|
+
authState: "delegated",
|
|
33
|
+
authLabel: "Authenticated via Codex",
|
|
34
|
+
statusMessage: "Authentication is managed via Codexa.",
|
|
35
|
+
supportsModels: () => true,
|
|
36
|
+
run: (prompt, options, handlers) => {
|
|
37
|
+
let done = false;
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
let proc: ReturnType<typeof spawn> | null = null;
|
|
40
|
+
let procExited = false;
|
|
41
|
+
let currentRawOutput = "";
|
|
42
|
+
let finalAnswerObserved = false;
|
|
43
|
+
|
|
44
|
+
const finishError = (message: string) => {
|
|
45
|
+
if (done) return;
|
|
46
|
+
done = true;
|
|
47
|
+
handlers.onError(message, currentRawOutput);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const finishSuccess = (response: string) => {
|
|
51
|
+
if (done) return;
|
|
52
|
+
done = true;
|
|
53
|
+
handlers.onResponse(response);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const emitFinalAnswerObserved = (response: string) => {
|
|
57
|
+
if (finalAnswerObserved) return;
|
|
58
|
+
finalAnswerObserved = true;
|
|
59
|
+
handlers.onFinalAnswerObserved?.(response);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const startAttempt = (structuredOutput: boolean, probeCapabilities = false) => {
|
|
63
|
+
if (cancelled || done) return;
|
|
64
|
+
let firstChunkSeen = false;
|
|
65
|
+
let firstStderrSeen = false;
|
|
66
|
+
handlers.benchmarkHooks?.onProviderPrepStart?.();
|
|
67
|
+
void prepareCodexExecLaunch(
|
|
68
|
+
{
|
|
69
|
+
runtime: options.runtime,
|
|
70
|
+
cwd: options.workspaceRoot,
|
|
71
|
+
structuredOutput,
|
|
72
|
+
probeCapabilities,
|
|
73
|
+
codexCommandPath: options.runtime.codexCommandPath,
|
|
74
|
+
},
|
|
75
|
+
import.meta.url,
|
|
76
|
+
)
|
|
77
|
+
.then((launchPlan) => {
|
|
78
|
+
if (cancelled || done) return;
|
|
79
|
+
if (!launchPlan.ok) {
|
|
80
|
+
finishError(launchPlan.error);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!launchPlan.executable) {
|
|
84
|
+
finishError("Codex launch preparation did not return an executable.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
handlers.benchmarkHooks?.onProviderPrepComplete?.();
|
|
88
|
+
|
|
89
|
+
let rawStdout = "";
|
|
90
|
+
let rawStderr = "";
|
|
91
|
+
let stdoutLineBuffer = "";
|
|
92
|
+
let mode: "undecided" | "json" | "legacy" = structuredOutput ? "undecided" : "legacy";
|
|
93
|
+
let legacyProgressSequence = 0;
|
|
94
|
+
|
|
95
|
+
// Consecutive thinking lines from the transcript parser are coalesced into
|
|
96
|
+
// a single progress update so the UI shows one accumulating block rather
|
|
97
|
+
// than a rapid series of individual entries. The sequence resets whenever
|
|
98
|
+
// an assistant delta or tool activity event breaks the run of thinking lines.
|
|
99
|
+
let activeTranscriptThinkingId: string | null = null;
|
|
100
|
+
let activeTranscriptThinkingText = "";
|
|
101
|
+
|
|
102
|
+
const resetTranscriptCoalescing = () => {
|
|
103
|
+
activeTranscriptThinkingId = null;
|
|
104
|
+
activeTranscriptThinkingText = "";
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const emitLegacyProgress = (source: "stdout" | "stderr" | "transcript", text: string) => {
|
|
108
|
+
if (isProcessTerminationNoise(text)) return;
|
|
109
|
+
const displayText = text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
110
|
+
handlers.onProgress?.({
|
|
111
|
+
id: `${source}-${++legacyProgressSequence}`,
|
|
112
|
+
source,
|
|
113
|
+
text: displayText,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const transcriptParser = createCodexTranscriptStreamParser({
|
|
118
|
+
onThinkingLine: (line) => {
|
|
119
|
+
if (activeTranscriptThinkingId === null) {
|
|
120
|
+
activeTranscriptThinkingId = `transcript-thinking-${++legacyProgressSequence}`;
|
|
121
|
+
activeTranscriptThinkingText = line;
|
|
122
|
+
} else {
|
|
123
|
+
activeTranscriptThinkingText = `${activeTranscriptThinkingText}\n${line}`;
|
|
124
|
+
}
|
|
125
|
+
handlers.onProgress?.({
|
|
126
|
+
id: activeTranscriptThinkingId,
|
|
127
|
+
source: "transcript",
|
|
128
|
+
text: activeTranscriptThinkingText,
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
onAssistantDelta: (chunk) => {
|
|
132
|
+
resetTranscriptCoalescing();
|
|
133
|
+
handlers.onAssistantDelta?.(chunk);
|
|
134
|
+
},
|
|
135
|
+
onToolActivity: (activity) => {
|
|
136
|
+
resetTranscriptCoalescing();
|
|
137
|
+
handlers.onToolActivity?.(activity);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
const transcriptStdoutSanitizer = createStdoutSanitizer();
|
|
141
|
+
const transcriptStderrSanitizer = createStdoutSanitizer();
|
|
142
|
+
const stdoutTitleStripper = createTerminalTitleSequenceStripper({
|
|
143
|
+
source: "src/core/providers/codexSubprocess.ts:codex.stdout",
|
|
144
|
+
stream: "stdout",
|
|
145
|
+
origin: "codex-cli",
|
|
146
|
+
});
|
|
147
|
+
const stderrTitleStripper = createTerminalTitleSequenceStripper({
|
|
148
|
+
source: "src/core/providers/codexSubprocess.ts:codex.stderr",
|
|
149
|
+
stream: "stderr",
|
|
150
|
+
origin: "codex-cli",
|
|
151
|
+
});
|
|
152
|
+
const jsonParser = createCodexJsonStreamParser({
|
|
153
|
+
onProgress: (update) => handlers.onProgress?.(update),
|
|
154
|
+
onAssistantDelta: (chunk) => handlers.onAssistantDelta?.(chunk),
|
|
155
|
+
onFinalAnswerObserved: emitFinalAnswerObserved,
|
|
156
|
+
onToolActivity: (activity) => handlers.onToolActivity?.(activity),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const feedTranscript = (text: string, stream: "stdout" | "stderr") => {
|
|
160
|
+
const sanitizer = stream === "stdout" ? transcriptStdoutSanitizer : transcriptStderrSanitizer;
|
|
161
|
+
const clean = sanitizer.process(text);
|
|
162
|
+
if (clean) {
|
|
163
|
+
transcriptParser.feed(clean);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const switchToTranscriptFallback = () => {
|
|
168
|
+
if (mode === "legacy") return;
|
|
169
|
+
mode = "legacy";
|
|
170
|
+
if (rawStdout) {
|
|
171
|
+
feedTranscript(rawStdout, "stdout");
|
|
172
|
+
}
|
|
173
|
+
if (rawStderr) {
|
|
174
|
+
feedTranscript(rawStderr, "stderr");
|
|
175
|
+
}
|
|
176
|
+
stdoutLineBuffer = "";
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const processStructuredLines = (flush: boolean) => {
|
|
180
|
+
while (true) {
|
|
181
|
+
const newlineIndex = stdoutLineBuffer.indexOf("\n");
|
|
182
|
+
if (newlineIndex === -1) {
|
|
183
|
+
if (!flush) return;
|
|
184
|
+
if (!stdoutLineBuffer) return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const line = newlineIndex === -1
|
|
188
|
+
? stdoutLineBuffer
|
|
189
|
+
: stdoutLineBuffer.slice(0, newlineIndex);
|
|
190
|
+
stdoutLineBuffer = newlineIndex === -1 ? "" : stdoutLineBuffer.slice(newlineIndex + 1);
|
|
191
|
+
const normalizedLine = line.replace(/\r$/, "");
|
|
192
|
+
if (!normalizedLine.trim()) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const parsed = jsonParser.feedLine(normalizedLine);
|
|
197
|
+
if (!parsed) {
|
|
198
|
+
if (mode === "json") {
|
|
199
|
+
emitLegacyProgress("stdout", normalizedLine);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
switchToTranscriptFallback();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
mode = "json";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const ingestStdoutText = (text: string) => {
|
|
210
|
+
if (!text) return;
|
|
211
|
+
currentRawOutput += text;
|
|
212
|
+
rawStdout += text;
|
|
213
|
+
|
|
214
|
+
if (mode === "legacy") {
|
|
215
|
+
feedTranscript(text, "stdout");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
stdoutLineBuffer += text;
|
|
220
|
+
processStructuredLines(false);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const ingestStderrText = (text: string) => {
|
|
224
|
+
if (!text) return;
|
|
225
|
+
currentRawOutput += text;
|
|
226
|
+
rawStderr += text;
|
|
227
|
+
|
|
228
|
+
if (mode === "legacy") {
|
|
229
|
+
feedTranscript(text, "stderr");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const lines = stripNonPrintableControls(stripAnsi(text))
|
|
234
|
+
.replace(/\r\n/g, "\n")
|
|
235
|
+
.replace(/\r/g, "\n")
|
|
236
|
+
.split("\n");
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
const trimmed = line.trim();
|
|
239
|
+
if (!trimmed || isStderrNoise(trimmed) || isProcessTerminationNoise(trimmed)) continue;
|
|
240
|
+
emitLegacyProgress("stderr", trimmed);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
handlers.onProcessLifecycle?.("before-spawn");
|
|
245
|
+
proc = spawnCodexProcess(launchPlan.executable, launchPlan.args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
246
|
+
procExited = false;
|
|
247
|
+
handlers.onProcessLifecycle?.("spawned");
|
|
248
|
+
handlers.benchmarkHooks?.onCodexProcessSpawned?.({
|
|
249
|
+
executable: launchPlan.executable,
|
|
250
|
+
argv: launchPlan.args,
|
|
251
|
+
});
|
|
252
|
+
perf.mark("spawn_done");
|
|
253
|
+
|
|
254
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
255
|
+
if (cancelled || done) return;
|
|
256
|
+
if (!firstChunkSeen) {
|
|
257
|
+
firstChunkSeen = true;
|
|
258
|
+
perf.mark("first_chunk");
|
|
259
|
+
handlers.benchmarkHooks?.onFirstStdout?.();
|
|
260
|
+
}
|
|
261
|
+
perf.mark("last_chunk");
|
|
262
|
+
ingestStdoutText(stdoutTitleStripper.process(chunk));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
266
|
+
if (cancelled || done) return;
|
|
267
|
+
if (!firstStderrSeen) {
|
|
268
|
+
firstStderrSeen = true;
|
|
269
|
+
handlers.benchmarkHooks?.onFirstStderr?.();
|
|
270
|
+
}
|
|
271
|
+
ingestStderrText(stderrTitleStripper.process(chunk));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
proc.on("close", (code) => {
|
|
275
|
+
procExited = true;
|
|
276
|
+
handlers.onProcessLifecycle?.("exit");
|
|
277
|
+
if (cancelled || done) return;
|
|
278
|
+
ingestStdoutText(stdoutTitleStripper.flush());
|
|
279
|
+
ingestStderrText(stderrTitleStripper.flush());
|
|
280
|
+
if (!firstChunkSeen) {
|
|
281
|
+
handlers.benchmarkHooks?.onFirstStdout?.(false);
|
|
282
|
+
}
|
|
283
|
+
if (!firstStderrSeen) {
|
|
284
|
+
handlers.benchmarkHooks?.onFirstStderr?.(false);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (mode !== "legacy") {
|
|
288
|
+
processStructuredLines(true);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (mode === "legacy") {
|
|
292
|
+
const remainingStdout = transcriptStdoutSanitizer.flush();
|
|
293
|
+
if (remainingStdout) transcriptParser.feed(remainingStdout);
|
|
294
|
+
const remainingStderr = transcriptStderrSanitizer.flush();
|
|
295
|
+
if (remainingStderr) transcriptParser.feed(remainingStderr);
|
|
296
|
+
transcriptParser.flush();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
handlers.benchmarkHooks?.onCodexProcessExit?.(code);
|
|
300
|
+
|
|
301
|
+
if (
|
|
302
|
+
structuredOutput
|
|
303
|
+
&& code !== 0
|
|
304
|
+
&& !jsonParser.hasStructuredEvents()
|
|
305
|
+
&& looksLikeUnsupportedStructuredOutput(`${rawStdout}\n${rawStderr}`)
|
|
306
|
+
) {
|
|
307
|
+
currentRawOutput = "";
|
|
308
|
+
startAttempt(false, true);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const structuredFailure = jsonParser.getFailureMessage();
|
|
313
|
+
if (structuredFailure) {
|
|
314
|
+
finishError(structuredFailure);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (code === 0) {
|
|
319
|
+
const finalResponse = mode === "json"
|
|
320
|
+
? jsonParser.getFinalResponse().trim() || sanitizeCodexTranscript(currentRawOutput)
|
|
321
|
+
: sanitizeCodexTranscript(currentRawOutput);
|
|
322
|
+
emitFinalAnswerObserved(finalResponse);
|
|
323
|
+
finishSuccess(finalResponse);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
finishError(`Process exited with code ${code}`);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
proc.on("error", (err) => {
|
|
331
|
+
handlers.onProcessLifecycle?.("error");
|
|
332
|
+
if (cancelled || done) return;
|
|
333
|
+
const errno = err as NodeJS.ErrnoException;
|
|
334
|
+
finishError(formatCodexLaunchError(errno));
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const promptPolicy = options.promptPolicy ?? "wrapped";
|
|
338
|
+
const providerPrompt = promptPolicy === "raw"
|
|
339
|
+
? prompt
|
|
340
|
+
: buildCodexPrompt(prompt, options.runtime, undefined, {
|
|
341
|
+
projectInstructions: options.projectInstructions,
|
|
342
|
+
});
|
|
343
|
+
handlers.benchmarkHooks?.onProviderPromptPrepared?.({
|
|
344
|
+
policy: promptPolicy,
|
|
345
|
+
characterCount: providerPrompt.length,
|
|
346
|
+
});
|
|
347
|
+
proc.stdin?.write(providerPrompt);
|
|
348
|
+
proc.stdin?.end();
|
|
349
|
+
})
|
|
350
|
+
.catch((error) => {
|
|
351
|
+
const errno = error as NodeJS.ErrnoException;
|
|
352
|
+
finishError(formatCodexLaunchError(errno));
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
currentRawOutput = "";
|
|
357
|
+
startAttempt(true);
|
|
358
|
+
|
|
359
|
+
return () => {
|
|
360
|
+
cancelled = true;
|
|
361
|
+
done = true;
|
|
362
|
+
handlers.benchmarkHooks?.onCleanupStart?.();
|
|
363
|
+
handlers.onProcessLifecycle?.("cleanup");
|
|
364
|
+
if (!proc || procExited || proc.killed) {
|
|
365
|
+
handlers.benchmarkHooks?.onCleanupComplete?.({ skipped: true });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
proc.kill();
|
|
369
|
+
handlers.benchmarkHooks?.onCleanupComplete?.({ skipped: false });
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { RunToolActivity } from "../../session/types.js";
|
|
4
|
+
import {
|
|
5
|
+
createCodexTranscriptStreamParser,
|
|
6
|
+
sanitizeCodexTranscript,
|
|
7
|
+
stripNonPrintableControls,
|
|
8
|
+
} from "./codexTranscript.js";
|
|
9
|
+
|
|
10
|
+
test("extracts the assistant reply from noisy codex transcript output", () => {
|
|
11
|
+
const raw = [
|
|
12
|
+
"Reading additional input from stdin...",
|
|
13
|
+
"OpenAI Codex v0.118.0 (research preview)",
|
|
14
|
+
"--------",
|
|
15
|
+
"workdir: C:\\Development\\1-JavaScript\\13-Custom CLI",
|
|
16
|
+
"model: gpt-5.4",
|
|
17
|
+
"provider: openai",
|
|
18
|
+
"approval: never",
|
|
19
|
+
"sandbox: read-only",
|
|
20
|
+
"reasoning effort: xhigh",
|
|
21
|
+
"reasoning summaries: none",
|
|
22
|
+
"session id: 019d4984-90c0-7402-80c9-3d55a8e0373f",
|
|
23
|
+
"--------",
|
|
24
|
+
"user",
|
|
25
|
+
"Hello",
|
|
26
|
+
"codex",
|
|
27
|
+
"Hello. What do you need help with?",
|
|
28
|
+
"tokens used",
|
|
29
|
+
"1,390",
|
|
30
|
+
"Hello. What do you need help with?",
|
|
31
|
+
].join("\n");
|
|
32
|
+
|
|
33
|
+
assert.equal(sanitizeCodexTranscript(raw), "Hello. What do you need help with?");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("falls back to filtered plain output when no labeled assistant block exists", () => {
|
|
37
|
+
const raw = [
|
|
38
|
+
"\u001b[32mOpenAI Codex v0.118.0\u001b[0m",
|
|
39
|
+
"--------",
|
|
40
|
+
"workdir: C:\\repo",
|
|
41
|
+
"",
|
|
42
|
+
"First useful line",
|
|
43
|
+
"Second useful line",
|
|
44
|
+
].join("\n");
|
|
45
|
+
|
|
46
|
+
assert.equal(sanitizeCodexTranscript(raw), "First useful line\nSecond useful line");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("filters echoed clarification-policy prompt lines from finalized transcripts", () => {
|
|
50
|
+
const raw = [
|
|
51
|
+
"default to best-effort continuation instead of stopping for clarification.",
|
|
52
|
+
"if a detail is missing but non-critical, make the most reasonable assumption and state it briefly.",
|
|
53
|
+
"if multiple paths are possible, choose one sensible path and continue.",
|
|
54
|
+
"only ask a blocking follow-up question if proceeding would likely use the wrong file, wrong command, destructive behavior, or produce fundamentally incorrect output.",
|
|
55
|
+
"if you are truly blocked on one critical missing fact, end the response with exactly one line in this format: [QUESTION]: <your question>",
|
|
56
|
+
"",
|
|
57
|
+
"Implemented the fix and assumed src/app.tsx was the target entrypoint.",
|
|
58
|
+
].join("\n");
|
|
59
|
+
|
|
60
|
+
assert.equal(
|
|
61
|
+
sanitizeCodexTranscript(raw),
|
|
62
|
+
"Implemented the fix and assumed src/app.tsx was the target entrypoint.",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("returns a readable fallback when only noise is present", () => {
|
|
67
|
+
const raw = [
|
|
68
|
+
"Reading additional input from stdin...",
|
|
69
|
+
"OpenAI Codex v0.118.0",
|
|
70
|
+
"--------",
|
|
71
|
+
"model: gpt-5.4",
|
|
72
|
+
"provider: openai",
|
|
73
|
+
].join("\n");
|
|
74
|
+
|
|
75
|
+
assert.match(
|
|
76
|
+
sanitizeCodexTranscript(raw),
|
|
77
|
+
/no assistant response text was detected/i,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("streams thinking lines separately from assistant deltas", () => {
|
|
82
|
+
const thinking: string[] = [];
|
|
83
|
+
const assistant: string[] = [];
|
|
84
|
+
const parser = createCodexTranscriptStreamParser({
|
|
85
|
+
onThinkingLine: (line) => thinking.push(line),
|
|
86
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
parser.feed([
|
|
90
|
+
"OpenAI Codex v0.118.0",
|
|
91
|
+
"Checking src/app.tsx",
|
|
92
|
+
"Task:",
|
|
93
|
+
"Refactor the CLI",
|
|
94
|
+
"",
|
|
95
|
+
"assistant",
|
|
96
|
+
"First line",
|
|
97
|
+
"Second line",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
parser.flush();
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(thinking, ["Checking src/app.tsx"]);
|
|
102
|
+
assert.deepEqual(assistant, ["First line", "\nSecond line"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("emits fenced code blocks atomically when streaming", () => {
|
|
106
|
+
const assistant: string[] = [];
|
|
107
|
+
const parser = createCodexTranscriptStreamParser({
|
|
108
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
parser.feed("assistant\n```ts\n");
|
|
112
|
+
parser.feed("const value = 1;\n");
|
|
113
|
+
parser.feed("```\n");
|
|
114
|
+
parser.flush();
|
|
115
|
+
|
|
116
|
+
// Code fences are buffered and emitted as a single atomic chunk
|
|
117
|
+
assert.deepEqual(assistant, ["```ts\nconst value = 1;\n```"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("emits tool activity separately from assistant prose while streaming", () => {
|
|
121
|
+
const assistant: string[] = [];
|
|
122
|
+
const toolActivity: RunToolActivity[] = [];
|
|
123
|
+
const parser = createCodexTranscriptStreamParser({
|
|
124
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
125
|
+
onToolActivity: (activity) => toolActivity.push(activity),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
parser.feed([
|
|
129
|
+
"assistant",
|
|
130
|
+
"$ rg --files",
|
|
131
|
+
"src/app.tsx",
|
|
132
|
+
"src/ui/BottomComposer.tsx",
|
|
133
|
+
"",
|
|
134
|
+
"I found the relevant files.",
|
|
135
|
+
].join("\n"));
|
|
136
|
+
parser.flush();
|
|
137
|
+
|
|
138
|
+
assert.deepEqual(assistant, ["I found the relevant files."]);
|
|
139
|
+
assert.equal(toolActivity.length, 2);
|
|
140
|
+
assert.equal(toolActivity[0]?.command, "rg --files");
|
|
141
|
+
assert.equal(toolActivity[0]?.status, "running");
|
|
142
|
+
assert.equal(toolActivity[1]?.command, "rg --files");
|
|
143
|
+
assert.equal(toolActivity[1]?.status, "completed");
|
|
144
|
+
assert.equal(toolActivity[1]?.summary, "Found 2 files");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("removes tool execution stdout from the finalized assistant transcript", () => {
|
|
148
|
+
const raw = [
|
|
149
|
+
"assistant",
|
|
150
|
+
"$ rg --files",
|
|
151
|
+
"src/app.tsx",
|
|
152
|
+
"src/ui/BottomComposer.tsx",
|
|
153
|
+
"",
|
|
154
|
+
"I found the relevant files and updated the composer.",
|
|
155
|
+
].join("\n");
|
|
156
|
+
|
|
157
|
+
assert.equal(
|
|
158
|
+
sanitizeCodexTranscript(raw),
|
|
159
|
+
"I found the relevant files and updated the composer.",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─── Progressive streaming tests ──────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
test("emits partial lines after timeout when no newline arrives", async () => {
|
|
166
|
+
const assistant: string[] = [];
|
|
167
|
+
const parser = createCodexTranscriptStreamParser({
|
|
168
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
parser.feed("assistant\n");
|
|
172
|
+
parser.feed("Hello wor");
|
|
173
|
+
// No newline yet — partial flush timer should fire after ~100ms
|
|
174
|
+
assert.equal(assistant.length, 0);
|
|
175
|
+
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
177
|
+
assert.equal(assistant.length, 1);
|
|
178
|
+
assert.equal(assistant[0], "Hello wor");
|
|
179
|
+
|
|
180
|
+
parser.flush();
|
|
181
|
+
// flush should not re-emit the already-emitted partial
|
|
182
|
+
assert.equal(assistant.length, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("does not duplicate content when partial is followed by complete line", async () => {
|
|
186
|
+
const assistant: string[] = [];
|
|
187
|
+
const parser = createCodexTranscriptStreamParser({
|
|
188
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
parser.feed("assistant\n");
|
|
192
|
+
parser.feed("Start of line");
|
|
193
|
+
// Wait for partial flush
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
195
|
+
assert.equal(assistant.length, 1);
|
|
196
|
+
assert.equal(assistant[0], "Start of line");
|
|
197
|
+
|
|
198
|
+
// Now the rest of the line arrives with newline
|
|
199
|
+
parser.feed(" and the rest\n");
|
|
200
|
+
// The partial was already emitted; the remaining text appends naturally
|
|
201
|
+
// The feed should process the complete line that includes both parts
|
|
202
|
+
assert.ok(assistant.length >= 1);
|
|
203
|
+
|
|
204
|
+
parser.flush();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("code fence with prose before and after emits prose immediately and fence atomically", () => {
|
|
208
|
+
const assistant: string[] = [];
|
|
209
|
+
const parser = createCodexTranscriptStreamParser({
|
|
210
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
parser.feed("assistant\nHere is the code:\n```ts\nconst x = 1;\n```\nDone.\n");
|
|
214
|
+
parser.flush();
|
|
215
|
+
|
|
216
|
+
assert.equal(assistant.length, 3);
|
|
217
|
+
assert.equal(assistant[0], "Here is the code:");
|
|
218
|
+
assert.equal(assistant[1], "\n```ts\nconst x = 1;\n```");
|
|
219
|
+
assert.equal(assistant[2], "\nDone.");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("code fence safety timeout emits buffered content after 3 seconds", async () => {
|
|
223
|
+
const assistant: string[] = [];
|
|
224
|
+
const parser = createCodexTranscriptStreamParser({
|
|
225
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
parser.feed("assistant\n```ts\nconst x = 1;\n");
|
|
229
|
+
// No closing fence — wait for safety timeout (3s)
|
|
230
|
+
// We'll test that flush() force-emits instead of waiting the full 3s
|
|
231
|
+
assert.equal(assistant.length, 0);
|
|
232
|
+
|
|
233
|
+
parser.flush();
|
|
234
|
+
assert.equal(assistant.length, 1);
|
|
235
|
+
assert.equal(assistant[0], "```ts\nconst x = 1;");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("auto-promotes long prose lines from preamble to assistant section", () => {
|
|
239
|
+
const thinking: string[] = [];
|
|
240
|
+
const assistant: string[] = [];
|
|
241
|
+
const parser = createCodexTranscriptStreamParser({
|
|
242
|
+
onThinkingLine: (line) => thinking.push(line),
|
|
243
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
parser.feed([
|
|
247
|
+
"I have analyzed the codebase and here is a comprehensive summary of all the changes that need to be made to fix this issue.",
|
|
248
|
+
].join("\n"));
|
|
249
|
+
parser.flush();
|
|
250
|
+
|
|
251
|
+
// Long prose with many words should auto-promote to assistant
|
|
252
|
+
assert.equal(thinking.length, 0);
|
|
253
|
+
assert.equal(assistant.length, 1);
|
|
254
|
+
assert.match(assistant[0]!, /analyzed the codebase/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("does not auto-promote short status lines from preamble", () => {
|
|
258
|
+
const thinking: string[] = [];
|
|
259
|
+
const assistant: string[] = [];
|
|
260
|
+
const parser = createCodexTranscriptStreamParser({
|
|
261
|
+
onThinkingLine: (line) => thinking.push(line),
|
|
262
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
parser.feed("Checking src/app.tsx\n");
|
|
266
|
+
parser.flush();
|
|
267
|
+
|
|
268
|
+
assert.equal(thinking.length, 1);
|
|
269
|
+
assert.equal(assistant.length, 0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("strips control characters from streamed and finalized assistant text", () => {
|
|
273
|
+
const assistant: string[] = [];
|
|
274
|
+
const parser = createCodexTranscriptStreamParser({
|
|
275
|
+
onAssistantDelta: (chunk) => assistant.push(chunk),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
parser.feed("assistant\nHello\u0007 world\u0001\n");
|
|
279
|
+
parser.flush();
|
|
280
|
+
|
|
281
|
+
assert.deepEqual(assistant, ["Hello world"]);
|
|
282
|
+
assert.equal(stripNonPrintableControls("A\u0000B\u001fC"), "ABC");
|
|
283
|
+
assert.equal(sanitizeCodexTranscript("assistant\nDone\u0007\n"), "Done");
|
|
284
|
+
});
|