@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,230 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
configureRenderDebug,
|
|
8
|
+
getRenderDebugLogPath,
|
|
9
|
+
traceBlankFrame,
|
|
10
|
+
traceEvent,
|
|
11
|
+
traceLayoutValidity,
|
|
12
|
+
traceLifecycleEvent,
|
|
13
|
+
traceLifecycleTransition,
|
|
14
|
+
traceFlickerEvent,
|
|
15
|
+
traceStatusTick,
|
|
16
|
+
traceRender,
|
|
17
|
+
traceTerminalWrite,
|
|
18
|
+
} from "./renderDebug.js";
|
|
19
|
+
|
|
20
|
+
function clean(path: string): void {
|
|
21
|
+
rmSync(path, { force: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("render debug stays quiet by default", () => {
|
|
25
|
+
const logPath = join(tmpdir(), `codexa-render-debug-quiet-${process.pid}.jsonl`);
|
|
26
|
+
clean(logPath);
|
|
27
|
+
|
|
28
|
+
configureRenderDebug({ CODEXA_RENDER_DEBUG_FILE: logPath });
|
|
29
|
+
traceEvent("test", "quiet");
|
|
30
|
+
traceRender("QuietComponent", "test");
|
|
31
|
+
|
|
32
|
+
assert.equal(existsSync(logPath), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("render debug writes JSONL only when explicitly enabled", () => {
|
|
36
|
+
const logPath = join(tmpdir(), `codexa-render-debug-enabled-${process.pid}.jsonl`);
|
|
37
|
+
clean(logPath);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
configureRenderDebug({
|
|
41
|
+
CODEXA_RENDER_DEBUG: "1",
|
|
42
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
43
|
+
});
|
|
44
|
+
traceRender("EnabledComponent", "unit");
|
|
45
|
+
|
|
46
|
+
assert.equal(getRenderDebugLogPath(), logPath);
|
|
47
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
48
|
+
assert.equal(records[0]?.kind, "session");
|
|
49
|
+
assert.equal(records[1]?.kind, "render");
|
|
50
|
+
assert.equal(records[1]?.component, "EnabledComponent");
|
|
51
|
+
assert.equal(records[1]?.reason, "unit");
|
|
52
|
+
} finally {
|
|
53
|
+
configureRenderDebug({});
|
|
54
|
+
clean(logPath);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("render debug defaults to the repo-local diagnostic log path", () => {
|
|
59
|
+
configureRenderDebug({});
|
|
60
|
+
assert.equal(getRenderDebugLogPath(), join(process.cwd(), ".codexa-debug", "render-debug.log"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("render debug creates missing log directories", () => {
|
|
64
|
+
const logPath = join(tmpdir(), `codexa-render-debug-nested-${process.pid}`, "render-debug.log");
|
|
65
|
+
rmSync(join(tmpdir(), `codexa-render-debug-nested-${process.pid}`), { force: true, recursive: true });
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
configureRenderDebug({
|
|
69
|
+
CODEXA_RENDER_DEBUG: "1",
|
|
70
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
71
|
+
});
|
|
72
|
+
assert.equal(existsSync(logPath), true);
|
|
73
|
+
} finally {
|
|
74
|
+
configureRenderDebug({});
|
|
75
|
+
rmSync(join(tmpdir(), `codexa-render-debug-nested-${process.pid}`), { force: true, recursive: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("render debug records lifecycle, layout, and blank-frame diagnostics", () => {
|
|
80
|
+
const logPath = join(tmpdir(), `codexa-render-diagnostics-${process.pid}.jsonl`);
|
|
81
|
+
clean(logPath);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
configureRenderDebug({
|
|
85
|
+
CODEXA_RENDER_DEBUG: "1",
|
|
86
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
87
|
+
});
|
|
88
|
+
traceLifecycleEvent("Timeline", "mount", { viewportRows: 12 });
|
|
89
|
+
traceLayoutValidity("Timeline", { viewportRows: 0, cols: 120 });
|
|
90
|
+
traceBlankFrame("Timeline", { reason: "visible-rows-zero-with-events" });
|
|
91
|
+
|
|
92
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
93
|
+
assert.equal(records[1]?.kind, "lifecycle");
|
|
94
|
+
assert.equal(records[1]?.component, "Timeline");
|
|
95
|
+
assert.equal(records[1]?.event, "mount");
|
|
96
|
+
assert.equal(records[2]?.kind, "layout");
|
|
97
|
+
assert.equal(records[2]?.event, "invalidLayout");
|
|
98
|
+
assert.deepEqual(records[2]?.invalidValues, [{ key: "viewportRows", value: 0 }]);
|
|
99
|
+
assert.equal(records[3]?.kind, "blankFrame");
|
|
100
|
+
assert.equal(records[3]?.reason, "visible-rows-zero-with-events");
|
|
101
|
+
} finally {
|
|
102
|
+
configureRenderDebug({});
|
|
103
|
+
clean(logPath);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("terminal writes are classified for clear and reset diagnosis", () => {
|
|
108
|
+
const logPath = join(tmpdir(), `codexa-terminal-classification-${process.pid}.jsonl`);
|
|
109
|
+
clean(logPath);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
configureRenderDebug({
|
|
113
|
+
CODEXA_RENDER_DEBUG: "1",
|
|
114
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
115
|
+
});
|
|
116
|
+
traceTerminalWrite(
|
|
117
|
+
"stdout",
|
|
118
|
+
"unit",
|
|
119
|
+
"\x1b[2J\x1b[3J\x1b[H\x1bc\x1b[?1049h\x1b]0;CODEXA\x07\x1b[?2004h\x1b[?1000h",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
123
|
+
assert.equal(records[1]?.kind, "stdout");
|
|
124
|
+
assert.equal(records[1]?.containsViewportClear, true);
|
|
125
|
+
assert.equal(records[1]?.containsScrollbackClear, true);
|
|
126
|
+
assert.equal(records[1]?.containsCursorHome, true);
|
|
127
|
+
assert.equal(records[1]?.containsTerminalReset, true);
|
|
128
|
+
assert.equal(records[1]?.containsAlternateScreen, true);
|
|
129
|
+
assert.equal(records[1]?.containsTitleSequence, true);
|
|
130
|
+
assert.equal(records[1]?.containsBracketedPaste, true);
|
|
131
|
+
assert.equal(records[1]?.containsMouseMode, true);
|
|
132
|
+
} finally {
|
|
133
|
+
configureRenderDebug({});
|
|
134
|
+
clean(logPath);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("CODEXA_DEBUG_RENDER aliases render debug logging", () => {
|
|
139
|
+
const logPath = join(tmpdir(), `codexa-debug-render-alias-${process.pid}.jsonl`);
|
|
140
|
+
clean(logPath);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
configureRenderDebug({
|
|
144
|
+
CODEXA_DEBUG_RENDER: "1",
|
|
145
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
146
|
+
});
|
|
147
|
+
traceRender("AliasComponent", "unit");
|
|
148
|
+
|
|
149
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
150
|
+
assert.equal(records[0]?.kind, "session");
|
|
151
|
+
assert.equal(records[1]?.kind, "render");
|
|
152
|
+
assert.equal(records[1]?.component, "AliasComponent");
|
|
153
|
+
} finally {
|
|
154
|
+
configureRenderDebug({});
|
|
155
|
+
clean(logPath);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("render trace flag enables compact render diagnostics", () => {
|
|
160
|
+
const logPath = join(tmpdir(), `codexa-render-trace-${process.pid}.jsonl`);
|
|
161
|
+
clean(logPath);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
configureRenderDebug({
|
|
165
|
+
CODEXA_DEBUG_RENDER_TRACE: "1",
|
|
166
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
167
|
+
});
|
|
168
|
+
traceRender("TraceComponent", "unit");
|
|
169
|
+
traceFlickerEvent("viewportSlice", { reason: "unit" });
|
|
170
|
+
|
|
171
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
172
|
+
assert.equal(records[0]?.kind, "session");
|
|
173
|
+
assert.equal(records[1]?.kind, "render");
|
|
174
|
+
assert.equal(records[1]?.component, "TraceComponent");
|
|
175
|
+
assert.equal(records[2]?.kind, "flicker");
|
|
176
|
+
assert.equal(records[2]?.event, "viewportSlice");
|
|
177
|
+
} finally {
|
|
178
|
+
configureRenderDebug({});
|
|
179
|
+
clean(logPath);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("lifecycle trace is gated by CODEXA_DEBUG_LIFECYCLE", () => {
|
|
184
|
+
const logPath = join(tmpdir(), `codexa-lifecycle-debug-${process.pid}.jsonl`);
|
|
185
|
+
clean(logPath);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
configureRenderDebug({
|
|
189
|
+
CODEXA_DEBUG_LIFECYCLE: "1",
|
|
190
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
191
|
+
});
|
|
192
|
+
traceLifecycleTransition({
|
|
193
|
+
prevKind: "THINKING",
|
|
194
|
+
nextKind: "IDLE",
|
|
195
|
+
reason: "unit",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
199
|
+
assert.equal(records.length, 1);
|
|
200
|
+
assert.equal(records[0]?.kind, "lifecycle");
|
|
201
|
+
assert.equal(records[0]?.reason, "unit");
|
|
202
|
+
} finally {
|
|
203
|
+
configureRenderDebug({});
|
|
204
|
+
clean(logPath);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("flicker trace is gated by CODEXA_DEBUG_FLICKER", () => {
|
|
209
|
+
const logPath = join(tmpdir(), `codexa-flicker-debug-${process.pid}.jsonl`);
|
|
210
|
+
clean(logPath);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
configureRenderDebug({
|
|
214
|
+
CODEXA_DEBUG_FLICKER: "1",
|
|
215
|
+
CODEXA_RENDER_DEBUG_FILE: logPath,
|
|
216
|
+
});
|
|
217
|
+
traceFlickerEvent("timelineRender", { reason: "unit" });
|
|
218
|
+
traceStatusTick({ owner: "Status", label: "Codex is thinking" });
|
|
219
|
+
|
|
220
|
+
const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
221
|
+
assert.equal(records.length, 2);
|
|
222
|
+
assert.equal(records[0]?.kind, "flicker");
|
|
223
|
+
assert.equal(records[0]?.event, "timelineRender");
|
|
224
|
+
assert.equal(records[1]?.kind, "flicker");
|
|
225
|
+
assert.equal(records[1]?.event, "statusTick");
|
|
226
|
+
} finally {
|
|
227
|
+
configureRenderDebug({});
|
|
228
|
+
clean(logPath);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
type DebugEnv = Record<string, string | undefined>;
|
|
6
|
+
|
|
7
|
+
// Global debug state — populated lazily on first check, or eagerly via configureRenderDebug().
|
|
8
|
+
let configured = false;
|
|
9
|
+
let enabled = false;
|
|
10
|
+
let renderTraceEnabled = false;
|
|
11
|
+
let lifecycleEnabled = false;
|
|
12
|
+
let flickerEnabled = false;
|
|
13
|
+
let plainActionsEnabled = false;
|
|
14
|
+
let logPath = join(process.cwd(), ".codexa-debug", "render-debug.log");
|
|
15
|
+
let sessionId = `${Date.now()}-${process.pid}`;
|
|
16
|
+
const counters = new Map<string, number>();
|
|
17
|
+
|
|
18
|
+
function configureFromEnv(env: DebugEnv = process.env): void {
|
|
19
|
+
renderTraceEnabled = env["CODEXA_DEBUG_RENDER_TRACE"] === "1";
|
|
20
|
+
// Both CODEXA_RENDER_DEBUG and CODEXA_DEBUG_RENDER activate render debugging —
|
|
21
|
+
// two names exist for historical reasons; either one is sufficient.
|
|
22
|
+
enabled = env["CODEXA_RENDER_DEBUG"] === "1"
|
|
23
|
+
|| env["CODEXA_DEBUG_RENDER"] === "1"
|
|
24
|
+
|| renderTraceEnabled;
|
|
25
|
+
lifecycleEnabled = env["CODEXA_DEBUG_LIFECYCLE"] === "1";
|
|
26
|
+
flickerEnabled = env["CODEXA_DEBUG_FLICKER"] === "1";
|
|
27
|
+
plainActionsEnabled = env["CODEXA_DEBUG_PLAIN_ACTIONS"] === "1";
|
|
28
|
+
logPath = env["CODEXA_RENDER_DEBUG_FILE"]?.trim() || join(process.cwd(), ".codexa-debug", "render-debug.log");
|
|
29
|
+
sessionId = `${Date.now()}-${process.pid}`;
|
|
30
|
+
configured = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function configureRenderDebug(env: DebugEnv = process.env): void {
|
|
34
|
+
configureFromEnv(env);
|
|
35
|
+
counters.clear();
|
|
36
|
+
if (enabled) {
|
|
37
|
+
writeRecord("session", { event: "start" });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isRenderDebugEnabled(): boolean {
|
|
42
|
+
if (!configured) {
|
|
43
|
+
configureFromEnv();
|
|
44
|
+
}
|
|
45
|
+
return enabled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isRenderTraceEnabled(): boolean {
|
|
49
|
+
if (!configured) {
|
|
50
|
+
configureFromEnv();
|
|
51
|
+
}
|
|
52
|
+
return renderTraceEnabled;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isLifecycleDebugEnabled(): boolean {
|
|
56
|
+
if (!configured) {
|
|
57
|
+
configureFromEnv();
|
|
58
|
+
}
|
|
59
|
+
return lifecycleEnabled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isFlickerDebugEnabled(): boolean {
|
|
63
|
+
if (!configured) {
|
|
64
|
+
configureFromEnv();
|
|
65
|
+
}
|
|
66
|
+
return flickerEnabled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isPlainActionsDebugEnabled(): boolean {
|
|
70
|
+
if (!configured) {
|
|
71
|
+
configureFromEnv();
|
|
72
|
+
}
|
|
73
|
+
return plainActionsEnabled;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getRenderDebugLogPath(): string {
|
|
77
|
+
if (!configured) {
|
|
78
|
+
configureFromEnv();
|
|
79
|
+
}
|
|
80
|
+
return logPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function nextCounter(name: string, by = 1): number {
|
|
84
|
+
const next = (counters.get(name) ?? 0) + by;
|
|
85
|
+
counters.set(name, next);
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sanitizeValue(value: unknown): unknown {
|
|
90
|
+
if (value == null) return value;
|
|
91
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value.map(sanitizeValue);
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "object") {
|
|
98
|
+
const record: Record<string, unknown> = {};
|
|
99
|
+
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
|
100
|
+
record[key] = sanitizeValue(nested);
|
|
101
|
+
}
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeRecord(kind: string, fields: Record<string, unknown>): void {
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
110
|
+
appendFileSync(
|
|
111
|
+
logPath,
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
ts: Date.now(),
|
|
114
|
+
pid: process.pid,
|
|
115
|
+
sessionId,
|
|
116
|
+
kind,
|
|
117
|
+
...(sanitizeValue(fields) as Record<string, unknown>),
|
|
118
|
+
}) + "\n",
|
|
119
|
+
"utf8",
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
// Debug logging must never disturb the TUI.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function traceLifecycleEvent(
|
|
127
|
+
component: string,
|
|
128
|
+
event: "mount" | "unmount" | "blankFrame" | "emptyFrame" | "stateTransition" | string,
|
|
129
|
+
fields: Record<string, unknown> = {},
|
|
130
|
+
): void {
|
|
131
|
+
if (!isRenderDebugEnabled()) return;
|
|
132
|
+
const count = nextCounter(`lifecycle.${component}.${event}`);
|
|
133
|
+
writeRecord("lifecycle", { component, event, count, ...fields });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useLifecycleDebug(
|
|
137
|
+
component: string,
|
|
138
|
+
fields: Record<string, unknown> = {},
|
|
139
|
+
): void {
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
traceLifecycleEvent(component, "mount", fields);
|
|
142
|
+
return () => {
|
|
143
|
+
traceLifecycleEvent(component, "unmount", fields);
|
|
144
|
+
};
|
|
145
|
+
}, []);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function traceBlankFrame(
|
|
149
|
+
component: string,
|
|
150
|
+
fields: Record<string, unknown> = {},
|
|
151
|
+
): void {
|
|
152
|
+
if (!isRenderDebugEnabled()) return;
|
|
153
|
+
const count = nextCounter(`blankFrame.${component}`);
|
|
154
|
+
writeRecord("blankFrame", { component, event: "blankFrame", count, ...fields });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function traceLayoutValidity(
|
|
158
|
+
component: string,
|
|
159
|
+
fields: Record<string, unknown> = {},
|
|
160
|
+
): void {
|
|
161
|
+
if (!isRenderDebugEnabled()) return;
|
|
162
|
+
const values = Object.entries(fields).filter(([, value]) => typeof value === "number") as Array<[string, number]>;
|
|
163
|
+
const invalidValues = values
|
|
164
|
+
.filter(([, value]) => !Number.isFinite(value) || value <= 0)
|
|
165
|
+
.map(([key, value]) => ({ key, value }));
|
|
166
|
+
const count = nextCounter(`layoutValidity.${component}`);
|
|
167
|
+
writeRecord("layout", {
|
|
168
|
+
event: invalidValues.length > 0 ? "invalidLayout" : "validLayout",
|
|
169
|
+
component,
|
|
170
|
+
count,
|
|
171
|
+
invalidValues,
|
|
172
|
+
...fields,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function traceStateTransition(fields: Record<string, unknown>): void {
|
|
177
|
+
if (!isRenderDebugEnabled() && !isLifecycleDebugEnabled()) return;
|
|
178
|
+
writeRecord("state", { event: "transition", ...fields });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function diffKeys(
|
|
182
|
+
previous: Record<string, unknown> | null,
|
|
183
|
+
next: Record<string, unknown>,
|
|
184
|
+
): string {
|
|
185
|
+
if (!previous) return "mount";
|
|
186
|
+
const changed: string[] = [];
|
|
187
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(next)]);
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
if (!Object.is(previous[key], next[key])) {
|
|
190
|
+
changed.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return changed.length > 0 ? changed.join(",") : "parent";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizeWatchedValue(value: unknown): unknown {
|
|
197
|
+
if (value == null) return value;
|
|
198
|
+
if (typeof value === "string") {
|
|
199
|
+
return value.length > 160 ? `${value.slice(0, 157)}...` : value;
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return { type: "array", length: value.length };
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "object") {
|
|
208
|
+
const record = value as Record<string, unknown>;
|
|
209
|
+
const reactType = record["type"];
|
|
210
|
+
if ("$$typeof" in record) {
|
|
211
|
+
return {
|
|
212
|
+
type: "reactElement",
|
|
213
|
+
name: typeof reactType === "string"
|
|
214
|
+
? reactType
|
|
215
|
+
: typeof reactType === "function"
|
|
216
|
+
? reactType.name
|
|
217
|
+
: "unknown",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (typeof record["kind"] === "string") {
|
|
221
|
+
return { type: "object", kind: record["kind"] };
|
|
222
|
+
}
|
|
223
|
+
if (typeof record["key"] === "string") {
|
|
224
|
+
return { type: "object", key: record["key"] };
|
|
225
|
+
}
|
|
226
|
+
return { type: "object", keys: Object.keys(record).slice(0, 8) };
|
|
227
|
+
}
|
|
228
|
+
return String(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function summarizeWatched(watched: Record<string, unknown>): Record<string, unknown> {
|
|
232
|
+
const summary: Record<string, unknown> = {};
|
|
233
|
+
for (const [key, value] of Object.entries(watched)) {
|
|
234
|
+
summary[key] = summarizeWatchedValue(value);
|
|
235
|
+
}
|
|
236
|
+
return summary;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function traceRender(
|
|
240
|
+
component: string,
|
|
241
|
+
reason = "unknown",
|
|
242
|
+
fields: Record<string, unknown> = {},
|
|
243
|
+
): void {
|
|
244
|
+
if (!isRenderDebugEnabled()) return;
|
|
245
|
+
const count = nextCounter(`render.${component}`);
|
|
246
|
+
writeRecord("render", { component, count, reason, ...fields });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function useRenderDebug(
|
|
250
|
+
component: string,
|
|
251
|
+
watched: Record<string, unknown> = {},
|
|
252
|
+
): void {
|
|
253
|
+
const renderCount = useRef(0);
|
|
254
|
+
const previous = useRef<Record<string, unknown> | null>(null);
|
|
255
|
+
renderCount.current += 1;
|
|
256
|
+
const reason = diffKeys(previous.current, watched);
|
|
257
|
+
if (isRenderDebugEnabled()) {
|
|
258
|
+
writeRecord("render", {
|
|
259
|
+
component,
|
|
260
|
+
count: renderCount.current,
|
|
261
|
+
reason,
|
|
262
|
+
watched: summarizeWatched(watched),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
previous.current = watched;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function useFlickerDebug(
|
|
269
|
+
event: string,
|
|
270
|
+
watched: Record<string, unknown> = {},
|
|
271
|
+
): void {
|
|
272
|
+
const renderCount = useRef(0);
|
|
273
|
+
const previous = useRef<Record<string, unknown> | null>(null);
|
|
274
|
+
renderCount.current += 1;
|
|
275
|
+
const reason = diffKeys(previous.current, watched);
|
|
276
|
+
if (isFlickerDebugEnabled() || isRenderTraceEnabled()) {
|
|
277
|
+
writeRecord("flicker", {
|
|
278
|
+
event,
|
|
279
|
+
count: renderCount.current,
|
|
280
|
+
reason,
|
|
281
|
+
watched: summarizeWatched(watched),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
previous.current = watched;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function traceEvent(
|
|
288
|
+
channel: string,
|
|
289
|
+
event: string,
|
|
290
|
+
fields: Record<string, unknown> = {},
|
|
291
|
+
): void {
|
|
292
|
+
if (!isRenderDebugEnabled()) return;
|
|
293
|
+
const count = nextCounter(`${channel}.${event}`);
|
|
294
|
+
writeRecord(channel, { event, count, ...fields });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function traceSchedulerFlush(fields: Record<string, unknown>): void {
|
|
298
|
+
traceEvent("scheduler", "flush", fields);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function traceStatusTick(fields: Record<string, unknown>): void {
|
|
302
|
+
traceEvent("status", "tick", fields);
|
|
303
|
+
traceFlickerEvent("statusTick", fields);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function traceTimelineUpdate(fields: Record<string, unknown>): void {
|
|
307
|
+
traceEvent("timeline", "update", fields);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set CODEXA_DEBUG_LIFECYCLE=1 to write one JSONL record for every UIState
|
|
312
|
+
* transition, including the derived composer and animation state.
|
|
313
|
+
*/
|
|
314
|
+
export function traceLifecycleTransition(fields: Record<string, unknown>): void {
|
|
315
|
+
if (!isLifecycleDebugEnabled()) return;
|
|
316
|
+
writeRecord("lifecycle", fields);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function traceFlickerEvent(event: string, fields: Record<string, unknown> = {}): void {
|
|
320
|
+
if (!isRenderDebugEnabled() && !isFlickerDebugEnabled() && !isRenderTraceEnabled()) return;
|
|
321
|
+
const count = nextCounter(`flicker.${event}`);
|
|
322
|
+
writeRecord("flicker", { event, count, ...fields });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function traceTerminalWrite(
|
|
326
|
+
stream: "stdout" | "stderr",
|
|
327
|
+
source: string,
|
|
328
|
+
chunk: unknown,
|
|
329
|
+
): void {
|
|
330
|
+
if (!isRenderDebugEnabled()) return;
|
|
331
|
+
const text = typeof chunk === "string"
|
|
332
|
+
? chunk
|
|
333
|
+
: chunk instanceof Uint8Array
|
|
334
|
+
? Buffer.from(chunk).toString("utf8")
|
|
335
|
+
: String(chunk ?? "");
|
|
336
|
+
writeRecord(stream, {
|
|
337
|
+
event: "directWrite",
|
|
338
|
+
count: nextCounter(`${stream}.directWrite`),
|
|
339
|
+
source,
|
|
340
|
+
bytes: Buffer.byteLength(text),
|
|
341
|
+
containsViewportClear: text.includes("\x1b[2J"),
|
|
342
|
+
containsScrollbackClear: text.includes("\x1b[3J"),
|
|
343
|
+
containsCursorHome: text.includes("\x1b[H"),
|
|
344
|
+
containsTerminalReset: text.includes("\x1bc"),
|
|
345
|
+
containsAlternateScreen: text.includes("\x1b[?1049h"),
|
|
346
|
+
containsTitleSequence: text.includes("\x1b]0;") || text.includes("\x1b]2;"),
|
|
347
|
+
containsBracketedPaste: text.includes("\x1b[?2004h") || text.includes("\x1b[?2004l"),
|
|
348
|
+
containsMouseMode: text.includes("\x1b[?1000h") || text.includes("\x1b[?1000l")
|
|
349
|
+
|| text.includes("\x1b[?1002h") || text.includes("\x1b[?1002l")
|
|
350
|
+
|| text.includes("\x1b[?1003h") || text.includes("\x1b[?1003l")
|
|
351
|
+
|| text.includes("\x1b[?1006h") || text.includes("\x1b[?1006l")
|
|
352
|
+
|| text.includes("\x1b[?1015h") || text.includes("\x1b[?1015l"),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function traceTerminalClear(source: string, fields: Record<string, unknown> = {}): void {
|
|
357
|
+
traceEvent("terminal", "clearScreen", { source, ...fields });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns accumulated render counts for all tracked components.
|
|
362
|
+
* Useful with `/debug renders` to verify that Header/Composer/Footer
|
|
363
|
+
* stay low during streaming while Timeline updates frequently.
|
|
364
|
+
*/
|
|
365
|
+
export function dumpRenderCounts(): Record<string, number> {
|
|
366
|
+
const result: Record<string, number> = {};
|
|
367
|
+
for (const [key, value] of counters) {
|
|
368
|
+
if (key.startsWith("render.")) {
|
|
369
|
+
result[key.slice("render.".length)] = value;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|