@golba98/codexa 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +396 -100
- package/bin/codexa.js +62 -144
- package/package.json +14 -8
- package/src/app.tsx +596 -306
- package/src/commands/handler.ts +6 -6
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/persistence.ts +10 -0
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +8 -16
- package/src/config/trustStore.ts +1 -1
- package/src/config/updateCheckCache.ts +19 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +65 -43
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/version/channel.ts +23 -0
- package/src/core/version/updateCheck.ts +193 -0
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +3 -2
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +670 -719
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +158 -118
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +222 -42
- package/src/ui/ModelReasoningPicker.tsx +15 -15
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +9 -9
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +8 -4
- package/src/ui/SettingsPanel.tsx +9 -9
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +11 -11
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +1625 -1472
- package/src/ui/TopHeader.tsx +437 -293
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +33 -33
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +197 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +299 -25
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +103 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +112 -0
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +218 -136
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codex.ts +0 -124
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/updateCheck.ts +0 -172
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
|
@@ -1,986 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import type { RunEvent, RunProgressEntry, RunToolActivity, UserPromptEvent } from "../session/types.js";
|
|
4
|
-
import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
|
|
5
|
-
import type { RenderTimelineItem } from "./Timeline.js";
|
|
6
|
-
import {
|
|
7
|
-
__clearTimelineMeasureCachesForTests,
|
|
8
|
-
__getStreamingBlockRowCacheSizeForTests,
|
|
9
|
-
__wrapStyledSpansForTests,
|
|
10
|
-
buildActionEventRows,
|
|
11
|
-
buildNativeTranscriptParts,
|
|
12
|
-
buildStableTimelineSnapshot,
|
|
13
|
-
buildTimelineSnapshot,
|
|
14
|
-
type StreamEvent,
|
|
15
|
-
type TimelineRowSpan,
|
|
16
|
-
} from "./timelineMeasure.js";
|
|
17
|
-
|
|
18
|
-
function makeTool(overrides: Partial<RunToolActivity> = {}): RunToolActivity {
|
|
19
|
-
return {
|
|
20
|
-
id: "tool-1",
|
|
21
|
-
command: "Get-Content README.md",
|
|
22
|
-
status: "completed",
|
|
23
|
-
startedAt: 10,
|
|
24
|
-
completedAt: 20,
|
|
25
|
-
summary: "Read 12 lines",
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeActionEvent(tool: RunToolActivity): Extract<StreamEvent, { kind: "action" }> {
|
|
31
|
-
return {
|
|
32
|
-
kind: "action",
|
|
33
|
-
streamSeq: 1,
|
|
34
|
-
tool,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function buildRows(tool: RunToolActivity, overrides: Partial<Parameters<typeof buildActionEventRows>[0]> = {}) {
|
|
39
|
-
return buildActionEventRows({
|
|
40
|
-
keyPrefix: "turn-1-action-1",
|
|
41
|
-
width: 80,
|
|
42
|
-
event: makeActionEvent(tool),
|
|
43
|
-
borderTone: "borderActive",
|
|
44
|
-
verbose: true,
|
|
45
|
-
isLive: false,
|
|
46
|
-
...overrides,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
test("buildActionEventRows reuses row array for stable tool inputs", () => {
|
|
51
|
-
__clearTimelineMeasureCachesForTests();
|
|
52
|
-
const tool = makeTool();
|
|
53
|
-
|
|
54
|
-
const first = buildRows(tool);
|
|
55
|
-
const second = buildRows(tool);
|
|
56
|
-
|
|
57
|
-
assert.strictEqual(second, first);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("buildActionEventRows preserves bordered action card shape when summary changes", () => {
|
|
61
|
-
__clearTimelineMeasureCachesForTests();
|
|
62
|
-
const first = buildRows(makeTool({ summary: "Read 12 lines" }));
|
|
63
|
-
const second = buildRows(makeTool({ summary: "Read 14 lines" }));
|
|
64
|
-
|
|
65
|
-
assert.equal(second.length, first.length);
|
|
66
|
-
assert.deepEqual(second.map((row) => row.key), first.map((row) => row.key));
|
|
67
|
-
assert.match(first.map((row) => row.spans.map((span) => span.text).join("")).join("\n"), /╭── action/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("completed action rows ignore live-target changes that do not render", () => {
|
|
71
|
-
__clearTimelineMeasureCachesForTests();
|
|
72
|
-
const tool = makeTool({ status: "completed" });
|
|
73
|
-
|
|
74
|
-
const first = buildRows(tool, { isLive: true });
|
|
75
|
-
const second = buildRows(tool, { isLive: false });
|
|
76
|
-
|
|
77
|
-
assert.strictEqual(second, first);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("running action rows keep their shape when live cursor display changes", () => {
|
|
81
|
-
__clearTimelineMeasureCachesForTests();
|
|
82
|
-
const tool = makeTool({ status: "running", completedAt: null });
|
|
83
|
-
|
|
84
|
-
const first = buildRows(tool, { isLive: true });
|
|
85
|
-
const second = buildRows(tool, { isLive: false });
|
|
86
|
-
|
|
87
|
-
assert.equal(second.length, first.length);
|
|
88
|
-
assert.match(first.map((row) => row.spans.map((span) => span.text).join("")).join("\n"), /▌/);
|
|
89
|
-
assert.doesNotMatch(second.map((row) => row.spans.map((span) => span.text).join("")).join("\n"), /▌/);
|
|
90
|
-
assert.deepEqual(second.map((row) => row.key), first.map((row) => row.key));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("running to completed action update keeps row count and keys stable", () => {
|
|
94
|
-
__clearTimelineMeasureCachesForTests();
|
|
95
|
-
|
|
96
|
-
const running = buildRows(makeTool({
|
|
97
|
-
status: "running",
|
|
98
|
-
completedAt: null,
|
|
99
|
-
summary: null,
|
|
100
|
-
}), { isLive: true });
|
|
101
|
-
const completed = buildRows(makeTool({
|
|
102
|
-
status: "completed",
|
|
103
|
-
completedAt: 42,
|
|
104
|
-
summary: "Read 12 lines",
|
|
105
|
-
}), { isLive: false });
|
|
106
|
-
|
|
107
|
-
assert.equal(completed.length, running.length);
|
|
108
|
-
assert.deepEqual(completed.map((row) => row.key), running.map((row) => row.key));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("action summary updates do not resize bordered action cards", () => {
|
|
112
|
-
__clearTimelineMeasureCachesForTests();
|
|
113
|
-
|
|
114
|
-
const first = buildRows(makeTool({ summary: null }));
|
|
115
|
-
const second = buildRows(makeTool({ summary: "Read 14 lines and summarized the file contents" }));
|
|
116
|
-
|
|
117
|
-
assert.equal(second.length, first.length);
|
|
118
|
-
assert.deepEqual(second.map((row) => row.key), first.map((row) => row.key));
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("bordered action cards show duration only after completion", () => {
|
|
122
|
-
__clearTimelineMeasureCachesForTests();
|
|
123
|
-
|
|
124
|
-
const running = buildRows(makeTool({ status: "running", completedAt: null }), { isLive: true });
|
|
125
|
-
const completed = buildRows(makeTool({ status: "completed", completedAt: 42 }), { isLive: false });
|
|
126
|
-
const runningText = running.map((row) => row.spans.map((span) => span.text).join("")).join("\n");
|
|
127
|
-
const completedText = completed.map((row) => row.spans.map((span) => span.text).join("")).join("\n");
|
|
128
|
-
|
|
129
|
-
assert.equal(running.length, completed.length);
|
|
130
|
-
assert.match(runningText, /╭── action/);
|
|
131
|
-
assert.match(runningText, /Read file/);
|
|
132
|
-
assert.doesNotMatch(runningText, /ms|s/);
|
|
133
|
-
assert.match(completedText, /Read file\s+32ms/);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("completed action rows ignore invisible timestamp changes when duration is unchanged", () => {
|
|
137
|
-
__clearTimelineMeasureCachesForTests();
|
|
138
|
-
|
|
139
|
-
const first = buildRows(makeTool({ startedAt: 10, completedAt: 20 }));
|
|
140
|
-
const second = buildRows(makeTool({ startedAt: 15, completedAt: 25 }));
|
|
141
|
-
|
|
142
|
-
assert.strictEqual(second, first);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("streaming row cache size is bounded", () => {
|
|
146
|
-
__clearTimelineMeasureCachesForTests();
|
|
147
|
-
|
|
148
|
-
for (let index = 0; index < 225; index += 1) {
|
|
149
|
-
buildRows(makeTool({
|
|
150
|
-
id: `tool-${index}`,
|
|
151
|
-
status: "running",
|
|
152
|
-
completedAt: null,
|
|
153
|
-
summary: `Read ${index} lines`,
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
assert.equal(__getStreamingBlockRowCacheSizeForTests(), 200);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
function makeProgressEntry(text: string): RunProgressEntry {
|
|
161
|
-
return {
|
|
162
|
-
id: "thinking-1",
|
|
163
|
-
source: "reasoning",
|
|
164
|
-
text,
|
|
165
|
-
sequence: 1,
|
|
166
|
-
createdAt: 1,
|
|
167
|
-
updatedAt: 1,
|
|
168
|
-
pendingNewlineCount: 0,
|
|
169
|
-
blocks: [{
|
|
170
|
-
id: "thinking-1-block-1",
|
|
171
|
-
text,
|
|
172
|
-
sequence: 1,
|
|
173
|
-
createdAt: 1,
|
|
174
|
-
updatedAt: 1,
|
|
175
|
-
status: "active",
|
|
176
|
-
streamSeq: 1,
|
|
177
|
-
}],
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function makeRun(thinkingText: string): RunEvent {
|
|
182
|
-
return {
|
|
183
|
-
id: 2,
|
|
184
|
-
type: "run",
|
|
185
|
-
createdAt: 1,
|
|
186
|
-
startedAt: 1,
|
|
187
|
-
durationMs: null,
|
|
188
|
-
backendId: "codex-subprocess",
|
|
189
|
-
backendLabel: "Codexa",
|
|
190
|
-
runtime: TEST_RUNTIME,
|
|
191
|
-
prompt: "Explain",
|
|
192
|
-
progressEntries: [makeProgressEntry(thinkingText)],
|
|
193
|
-
status: "running",
|
|
194
|
-
summary: "processing...",
|
|
195
|
-
truncatedOutput: false,
|
|
196
|
-
toolActivities: [makeTool({ streamSeq: 2 })],
|
|
197
|
-
activity: [],
|
|
198
|
-
touchedFileCount: 0,
|
|
199
|
-
errorMessage: null,
|
|
200
|
-
turnId: 1,
|
|
201
|
-
streamItems: [
|
|
202
|
-
{ streamSeq: 1, kind: "thinking", refId: "thinking-1-block-1" },
|
|
203
|
-
{ streamSeq: 2, kind: "action", refId: "tool-1" },
|
|
204
|
-
],
|
|
205
|
-
responseSegments: [],
|
|
206
|
-
lastStreamSeq: 2,
|
|
207
|
-
activeResponseSegmentId: null,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function makeRenderItem(thinkingText: string): RenderTimelineItem {
|
|
212
|
-
const user: UserPromptEvent = {
|
|
213
|
-
id: 1,
|
|
214
|
-
type: "user",
|
|
215
|
-
createdAt: 1,
|
|
216
|
-
prompt: "Explain",
|
|
217
|
-
turnId: 1,
|
|
218
|
-
};
|
|
219
|
-
return {
|
|
220
|
-
key: "turn-1",
|
|
221
|
-
type: "turn",
|
|
222
|
-
padded: true,
|
|
223
|
-
item: {
|
|
224
|
-
type: "turn",
|
|
225
|
-
turnId: 1,
|
|
226
|
-
turnIndex: 0,
|
|
227
|
-
user,
|
|
228
|
-
run: makeRun(thinkingText),
|
|
229
|
-
assistant: null,
|
|
230
|
-
},
|
|
231
|
-
renderState: {
|
|
232
|
-
opacity: "active",
|
|
233
|
-
question: null,
|
|
234
|
-
runPhase: "streaming",
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function makeStreamingResponseRenderItem(text: string): RenderTimelineItem {
|
|
240
|
-
const user: UserPromptEvent = {
|
|
241
|
-
id: 10,
|
|
242
|
-
type: "user",
|
|
243
|
-
createdAt: 1,
|
|
244
|
-
prompt: "Compare algorithms",
|
|
245
|
-
turnId: 2,
|
|
246
|
-
};
|
|
247
|
-
const run: RunEvent = {
|
|
248
|
-
id: 11,
|
|
249
|
-
type: "run",
|
|
250
|
-
createdAt: 1,
|
|
251
|
-
startedAt: 1,
|
|
252
|
-
durationMs: null,
|
|
253
|
-
backendId: "codex-subprocess",
|
|
254
|
-
backendLabel: "Codexa",
|
|
255
|
-
runtime: TEST_RUNTIME,
|
|
256
|
-
prompt: "Compare algorithms",
|
|
257
|
-
progressEntries: [],
|
|
258
|
-
status: "running",
|
|
259
|
-
summary: "streaming...",
|
|
260
|
-
truncatedOutput: false,
|
|
261
|
-
toolActivities: [],
|
|
262
|
-
activity: [],
|
|
263
|
-
touchedFileCount: 0,
|
|
264
|
-
errorMessage: null,
|
|
265
|
-
turnId: 2,
|
|
266
|
-
streamItems: [{ streamSeq: 1, kind: "response", refId: "response-1" }],
|
|
267
|
-
responseSegments: [{
|
|
268
|
-
id: "response-1",
|
|
269
|
-
streamSeq: 1,
|
|
270
|
-
chunks: [text],
|
|
271
|
-
status: "active",
|
|
272
|
-
startedAt: 1,
|
|
273
|
-
}],
|
|
274
|
-
lastStreamSeq: 1,
|
|
275
|
-
activeResponseSegmentId: "response-1",
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
key: "turn-2",
|
|
280
|
-
type: "turn",
|
|
281
|
-
padded: true,
|
|
282
|
-
item: {
|
|
283
|
-
type: "turn",
|
|
284
|
-
turnId: 2,
|
|
285
|
-
turnIndex: 1,
|
|
286
|
-
user,
|
|
287
|
-
run,
|
|
288
|
-
assistant: null,
|
|
289
|
-
},
|
|
290
|
-
renderState: {
|
|
291
|
-
opacity: "active",
|
|
292
|
-
question: null,
|
|
293
|
-
runPhase: "streaming",
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function makeActionSequenceRenderItem(tools: RunToolActivity[]): RenderTimelineItem {
|
|
299
|
-
const user: UserPromptEvent = {
|
|
300
|
-
id: 20,
|
|
301
|
-
type: "user",
|
|
302
|
-
createdAt: 1,
|
|
303
|
-
prompt: "Inspect files",
|
|
304
|
-
turnId: 3,
|
|
305
|
-
};
|
|
306
|
-
const run: RunEvent = {
|
|
307
|
-
id: 21,
|
|
308
|
-
type: "run",
|
|
309
|
-
createdAt: 1,
|
|
310
|
-
startedAt: 1,
|
|
311
|
-
durationMs: null,
|
|
312
|
-
backendId: "codex-subprocess",
|
|
313
|
-
backendLabel: "Codexa",
|
|
314
|
-
runtime: TEST_RUNTIME,
|
|
315
|
-
prompt: "Inspect files",
|
|
316
|
-
progressEntries: [],
|
|
317
|
-
status: "running",
|
|
318
|
-
summary: "running...",
|
|
319
|
-
truncatedOutput: false,
|
|
320
|
-
toolActivities: tools,
|
|
321
|
-
activity: [],
|
|
322
|
-
touchedFileCount: 0,
|
|
323
|
-
errorMessage: null,
|
|
324
|
-
turnId: 3,
|
|
325
|
-
streamItems: tools.map((tool) => ({
|
|
326
|
-
streamSeq: tool.streamSeq ?? 0,
|
|
327
|
-
kind: "action" as const,
|
|
328
|
-
refId: tool.id,
|
|
329
|
-
})),
|
|
330
|
-
responseSegments: [],
|
|
331
|
-
lastStreamSeq: tools.reduce((max, tool) => Math.max(max, tool.streamSeq ?? 0), 0),
|
|
332
|
-
activeResponseSegmentId: null,
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
key: "turn-3",
|
|
337
|
-
type: "turn",
|
|
338
|
-
padded: true,
|
|
339
|
-
item: {
|
|
340
|
-
type: "turn",
|
|
341
|
-
turnId: 3,
|
|
342
|
-
turnIndex: 2,
|
|
343
|
-
user,
|
|
344
|
-
run,
|
|
345
|
-
assistant: null,
|
|
346
|
-
},
|
|
347
|
-
renderState: {
|
|
348
|
-
opacity: "active",
|
|
349
|
-
question: null,
|
|
350
|
-
runPhase: "streaming",
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function makeCompletedPlanRenderItem(planText: string): RenderTimelineItem {
|
|
356
|
-
const user: UserPromptEvent = {
|
|
357
|
-
id: 30,
|
|
358
|
-
type: "user",
|
|
359
|
-
createdAt: 1,
|
|
360
|
-
prompt: "Plan a better architectural update to the file tree",
|
|
361
|
-
turnId: 4,
|
|
362
|
-
};
|
|
363
|
-
const run: RunEvent = {
|
|
364
|
-
id: 31,
|
|
365
|
-
type: "run",
|
|
366
|
-
createdAt: 1,
|
|
367
|
-
startedAt: 1,
|
|
368
|
-
durationMs: 100,
|
|
369
|
-
backendId: "codex-subprocess",
|
|
370
|
-
backendLabel: "Codexa",
|
|
371
|
-
runtime: TEST_RUNTIME,
|
|
372
|
-
prompt: user.prompt,
|
|
373
|
-
progressEntries: [],
|
|
374
|
-
status: "completed",
|
|
375
|
-
summary: "completed",
|
|
376
|
-
truncatedOutput: false,
|
|
377
|
-
toolActivities: [],
|
|
378
|
-
activity: [],
|
|
379
|
-
touchedFileCount: 0,
|
|
380
|
-
errorMessage: null,
|
|
381
|
-
turnId: 4,
|
|
382
|
-
streamItems: [{ streamSeq: 1, kind: "plan", refId: "plan-31" }],
|
|
383
|
-
responseSegments: [],
|
|
384
|
-
lastStreamSeq: 1,
|
|
385
|
-
activeResponseSegmentId: null,
|
|
386
|
-
plan: {
|
|
387
|
-
id: "plan-31",
|
|
388
|
-
streamSeq: 1,
|
|
389
|
-
chunks: planText ? [planText] : [],
|
|
390
|
-
status: "completed",
|
|
391
|
-
startedAt: 1,
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
key: "turn-plan-4",
|
|
397
|
-
type: "turn",
|
|
398
|
-
padded: true,
|
|
399
|
-
item: {
|
|
400
|
-
type: "turn",
|
|
401
|
-
turnId: 4,
|
|
402
|
-
turnIndex: 3,
|
|
403
|
-
user,
|
|
404
|
-
run,
|
|
405
|
-
assistant: null,
|
|
406
|
-
},
|
|
407
|
-
renderState: {
|
|
408
|
-
opacity: "active",
|
|
409
|
-
question: null,
|
|
410
|
-
runPhase: "none",
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function snapshotText(rows: Array<{ spans: Array<{ text: string }> }>): string {
|
|
416
|
-
return rows.map((row) => row.spans.map((span) => span.text).join("")).join("\n");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function stableRowsForTools(tools: RunToolActivity[]) {
|
|
420
|
-
return buildStableTimelineSnapshot(
|
|
421
|
-
[makeActionSequenceRenderItem(tools)],
|
|
422
|
-
{ totalWidth: 72, debugLabel: "action-sequence" },
|
|
423
|
-
).snapshot.rows;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function actionRows(rows: Array<{ key: string }>, streamSeq: number): string[] {
|
|
427
|
-
return rows
|
|
428
|
-
.map((row) => row.key)
|
|
429
|
-
.filter((key) => key.includes(`-action-${streamSeq}-`));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function actionTopIndex(rows: Array<{ key: string }>, streamSeq: number): number {
|
|
433
|
-
return rows.findIndex((row) => row.key.includes(`-action-${streamSeq}-top`));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
test("completed action wrapped rows stay stable when earlier thinking grows", () => {
|
|
437
|
-
__clearTimelineMeasureCachesForTests();
|
|
438
|
-
|
|
439
|
-
const first = buildTimelineSnapshot(
|
|
440
|
-
[makeRenderItem("Inspecting proof files.")],
|
|
441
|
-
{ totalWidth: 72, debugLabel: "test-before" },
|
|
442
|
-
);
|
|
443
|
-
const second = buildTimelineSnapshot(
|
|
444
|
-
[makeRenderItem("Inspecting proof files and reading surrounding documentation before summarizing the verification workflow.")],
|
|
445
|
-
{ totalWidth: 72, debugLabel: "test-after" },
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
const firstActionRows = first.rows.filter((row) => row.key.includes("-action-2-"));
|
|
449
|
-
const secondActionRows = second.rows.filter((row) => row.key.includes("-action-2-"));
|
|
450
|
-
|
|
451
|
-
assert.ok(firstActionRows.length > 0);
|
|
452
|
-
assert.equal(secondActionRows.length, firstActionRows.length);
|
|
453
|
-
assert.deepEqual(secondActionRows.map((row) => row.key), firstActionRows.map((row) => row.key));
|
|
454
|
-
for (let index = 0; index < firstActionRows.length; index += 1) {
|
|
455
|
-
assert.strictEqual(secondActionRows[index], firstActionRows[index]);
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test("active thinking rows are omitted while action rows remain visible", () => {
|
|
460
|
-
__clearTimelineMeasureCachesForTests();
|
|
461
|
-
|
|
462
|
-
const snapshot = buildTimelineSnapshot(
|
|
463
|
-
[makeRenderItem("Inspecting proof files.")],
|
|
464
|
-
{ totalWidth: 72, debugLabel: "active-thinking-omitted" },
|
|
465
|
-
);
|
|
466
|
-
const joined = snapshot.rows.map((row) => row.spans.map((span) => span.text).join("")).join("\n");
|
|
467
|
-
|
|
468
|
-
assert.doesNotMatch(joined, /Inspecting proof files/i);
|
|
469
|
-
assert.match(joined, /Read file/i);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
test("stable active action sequence appends without moving existing action keys", () => {
|
|
473
|
-
__clearTimelineMeasureCachesForTests();
|
|
474
|
-
|
|
475
|
-
const firstRunningRows = stableRowsForTools([
|
|
476
|
-
makeTool({ id: "tool-1", status: "running", completedAt: null, summary: null, streamSeq: 1 }),
|
|
477
|
-
]);
|
|
478
|
-
const firstCompletedRows = stableRowsForTools([
|
|
479
|
-
makeTool({ id: "tool-1", status: "completed", completedAt: 42, summary: "Read 12 lines", streamSeq: 1 }),
|
|
480
|
-
]);
|
|
481
|
-
const secondRunningRows = stableRowsForTools([
|
|
482
|
-
makeTool({ id: "tool-1", status: "completed", completedAt: 42, summary: "Read 12 lines", streamSeq: 1 }),
|
|
483
|
-
makeTool({ id: "tool-2", command: "Get-Content package.json", status: "running", completedAt: null, summary: null, streamSeq: 2 }),
|
|
484
|
-
]);
|
|
485
|
-
const bothCompletedRows = stableRowsForTools([
|
|
486
|
-
makeTool({ id: "tool-1", status: "completed", completedAt: 42, summary: "Read 12 lines", streamSeq: 1 }),
|
|
487
|
-
makeTool({ id: "tool-2", command: "Get-Content package.json", status: "completed", completedAt: 56, summary: "Read package", streamSeq: 2 }),
|
|
488
|
-
]);
|
|
489
|
-
|
|
490
|
-
const firstActionKeys = actionRows(firstRunningRows, 1);
|
|
491
|
-
assert.ok(firstActionKeys.length > 0);
|
|
492
|
-
assert.deepEqual(actionRows(firstCompletedRows, 1), firstActionKeys);
|
|
493
|
-
assert.deepEqual(actionRows(secondRunningRows, 1), firstActionKeys);
|
|
494
|
-
assert.deepEqual(actionRows(bothCompletedRows, 1), firstActionKeys);
|
|
495
|
-
assert.ok(actionTopIndex(secondRunningRows, 1) < actionTopIndex(secondRunningRows, 2));
|
|
496
|
-
assert.ok(actionTopIndex(bothCompletedRows, 1) < actionTopIndex(bothCompletedRows, 2));
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test("stable active action rows keep stream order when a later action completes first", () => {
|
|
500
|
-
__clearTimelineMeasureCachesForTests();
|
|
501
|
-
|
|
502
|
-
const rows = stableRowsForTools([
|
|
503
|
-
makeTool({ id: "tool-1", status: "running", completedAt: null, summary: null, streamSeq: 1 }),
|
|
504
|
-
makeTool({ id: "tool-2", command: "Get-Content package.json", status: "completed", completedAt: 56, summary: "Read package", streamSeq: 2 }),
|
|
505
|
-
]);
|
|
506
|
-
|
|
507
|
-
assert.ok(actionTopIndex(rows, 1) >= 0);
|
|
508
|
-
assert.ok(actionTopIndex(rows, 2) >= 0);
|
|
509
|
-
assert.ok(actionTopIndex(rows, 1) < actionTopIndex(rows, 2));
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
test("default stable timeline summarizes long repeated read action bursts", () => {
|
|
513
|
-
__clearTimelineMeasureCachesForTests();
|
|
514
|
-
|
|
515
|
-
const tools = Array.from({ length: 7 }, (_, index) => makeTool({
|
|
516
|
-
id: `tool-${index + 1}`,
|
|
517
|
-
command: `Get-Content file-${index + 1}.txt`,
|
|
518
|
-
status: "completed",
|
|
519
|
-
completedAt: 50 + index,
|
|
520
|
-
summary: `Read file ${index + 1}`,
|
|
521
|
-
streamSeq: index + 1,
|
|
522
|
-
}));
|
|
523
|
-
|
|
524
|
-
const rows = stableRowsForTools(tools);
|
|
525
|
-
const text = snapshotText(rows);
|
|
526
|
-
|
|
527
|
-
assert.match(text, /3 repeated read activity summarized/);
|
|
528
|
-
assert.ok(actionTopIndex(rows, 1) >= 0);
|
|
529
|
-
assert.ok(actionTopIndex(rows, 2) >= 0);
|
|
530
|
-
assert.ok(actionTopIndex(rows, 6) >= 0);
|
|
531
|
-
assert.ok(actionTopIndex(rows, 7) >= 0);
|
|
532
|
-
assert.equal(actionTopIndex(rows, 3), -1);
|
|
533
|
-
assert.equal(actionTopIndex(rows, 5), -1);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test("verbose stable timeline keeps every repeated read action visible", () => {
|
|
537
|
-
__clearTimelineMeasureCachesForTests();
|
|
538
|
-
|
|
539
|
-
const tools = Array.from({ length: 7 }, (_, index) => makeTool({
|
|
540
|
-
id: `tool-${index + 1}`,
|
|
541
|
-
command: `Get-Content file-${index + 1}.txt`,
|
|
542
|
-
status: "completed",
|
|
543
|
-
completedAt: 50 + index,
|
|
544
|
-
summary: `Read file ${index + 1}`,
|
|
545
|
-
streamSeq: index + 1,
|
|
546
|
-
}));
|
|
547
|
-
const rows = buildStableTimelineSnapshot(
|
|
548
|
-
[makeActionSequenceRenderItem(tools)],
|
|
549
|
-
{ totalWidth: 72, debugLabel: "verbose-action-sequence", verboseMode: true },
|
|
550
|
-
).snapshot.rows;
|
|
551
|
-
|
|
552
|
-
assert.doesNotMatch(snapshotText(rows), /repeated read activity summarized/);
|
|
553
|
-
for (let index = 1; index <= 7; index += 1) {
|
|
554
|
-
assert.ok(actionTopIndex(rows, index) >= 0, `tool ${index} should stay visible`);
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
test("stable timeline freezes completed action rows while active text changes", () => {
|
|
559
|
-
__clearTimelineMeasureCachesForTests();
|
|
560
|
-
|
|
561
|
-
const first = buildStableTimelineSnapshot(
|
|
562
|
-
[makeRenderItem("Inspecting proof files.")],
|
|
563
|
-
{ totalWidth: 72, debugLabel: "stable-before" },
|
|
564
|
-
);
|
|
565
|
-
const second = buildStableTimelineSnapshot(
|
|
566
|
-
[makeRenderItem("Inspecting proof files and reading surrounding documentation before summarizing the verification workflow.")],
|
|
567
|
-
{ totalWidth: 72, debugLabel: "stable-after" },
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
const firstActionRows = first.frozenRows.filter((row) => row.key.includes("-action-2-"));
|
|
571
|
-
const secondActionRows = second.frozenRows.filter((row) => row.key.includes("-action-2-"));
|
|
572
|
-
|
|
573
|
-
assert.ok(firstActionRows.length > 0);
|
|
574
|
-
assert.equal(secondActionRows.length, firstActionRows.length);
|
|
575
|
-
for (let index = 0; index < firstActionRows.length; index += 1) {
|
|
576
|
-
assert.strictEqual(secondActionRows[index], firstActionRows[index]);
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test("buildTimelineSnapshot re-renders when a completed plan changes from empty to final text", () => {
|
|
581
|
-
__clearTimelineMeasureCachesForTests();
|
|
582
|
-
|
|
583
|
-
const empty = buildTimelineSnapshot(
|
|
584
|
-
[makeCompletedPlanRenderItem("")],
|
|
585
|
-
{ totalWidth: 90, debugLabel: "plan-empty" },
|
|
586
|
-
);
|
|
587
|
-
const final = buildTimelineSnapshot(
|
|
588
|
-
[makeCompletedPlanRenderItem("## Final architecture plan\n1. Update the file tree renderer.")],
|
|
589
|
-
{ totalWidth: 90, debugLabel: "plan-final" },
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
assert.doesNotMatch(snapshotText(empty.rows), /Final architecture plan/);
|
|
593
|
-
assert.match(snapshotText(final.rows), /Final architecture plan/);
|
|
594
|
-
assert.match(snapshotText(final.rows), /Update the file tree renderer/);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
test("buildStableTimelineSnapshot re-renders final plan text under the same turn key", () => {
|
|
598
|
-
__clearTimelineMeasureCachesForTests();
|
|
599
|
-
|
|
600
|
-
const empty = buildStableTimelineSnapshot(
|
|
601
|
-
[makeCompletedPlanRenderItem("")],
|
|
602
|
-
{ totalWidth: 90, debugLabel: "stable-plan-empty" },
|
|
603
|
-
);
|
|
604
|
-
const final = buildStableTimelineSnapshot(
|
|
605
|
-
[makeCompletedPlanRenderItem("## Final architecture plan\n1. Update the file tree renderer.")],
|
|
606
|
-
{ totalWidth: 90, debugLabel: "stable-plan-final" },
|
|
607
|
-
);
|
|
608
|
-
|
|
609
|
-
assert.doesNotMatch(snapshotText(empty.snapshot.rows), /Final architecture plan/);
|
|
610
|
-
assert.match(snapshotText(final.snapshot.rows), /Final architecture plan/);
|
|
611
|
-
assert.match(snapshotText(final.snapshot.rows), /Update the file tree renderer/);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
test("unchanged active response rows keep references while streaming text grows", () => {
|
|
615
|
-
__clearTimelineMeasureCachesForTests();
|
|
616
|
-
|
|
617
|
-
const first = buildTimelineSnapshot(
|
|
618
|
-
[makeStreamingResponseRenderItem("Line one.\nLine two.")],
|
|
619
|
-
{ totalWidth: 72, debugLabel: "response-before" },
|
|
620
|
-
);
|
|
621
|
-
const second = buildTimelineSnapshot(
|
|
622
|
-
[makeStreamingResponseRenderItem("Line one.\nLine two.\nLine three is arriving.")],
|
|
623
|
-
{ totalWidth: 72, debugLabel: "response-after" },
|
|
624
|
-
);
|
|
625
|
-
|
|
626
|
-
const firstStableLine = first.rows.find((row) =>
|
|
627
|
-
row.key.includes("-codex-response-1-content-0")
|
|
628
|
-
&& row.spans.some((span) => span.text.includes("Line one.")),
|
|
629
|
-
);
|
|
630
|
-
const secondStableLine = second.rows.find((row) => row.key === firstStableLine?.key);
|
|
631
|
-
|
|
632
|
-
assert.ok(firstStableLine);
|
|
633
|
-
assert.strictEqual(secondStableLine, firstStableLine);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
test("native transcript parts keep all actions in liveRows during active run", () => {
|
|
637
|
-
__clearTimelineMeasureCachesForTests();
|
|
638
|
-
|
|
639
|
-
const parts = buildNativeTranscriptParts(
|
|
640
|
-
[makeActionSequenceRenderItem([
|
|
641
|
-
makeTool({ id: "tool-1", status: "completed", completedAt: 42, summary: "Read README", streamSeq: 1 }),
|
|
642
|
-
makeTool({
|
|
643
|
-
id: "tool-2",
|
|
644
|
-
command: "Get-Content package.json",
|
|
645
|
-
status: "running",
|
|
646
|
-
completedAt: null,
|
|
647
|
-
summary: null,
|
|
648
|
-
streamSeq: 2,
|
|
649
|
-
}),
|
|
650
|
-
])],
|
|
651
|
-
{ totalWidth: 72, debugLabel: "native-action-split" },
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
const staticKeys = parts.staticItems.flatMap((item) => item.rows.map((row) => row.key));
|
|
655
|
-
const liveKeys = parts.liveRows.map((row) => row.key);
|
|
656
|
-
|
|
657
|
-
// User prompt is always committed to staticItems immediately.
|
|
658
|
-
assert.ok(staticKeys.some((key) => key.includes("-user-")));
|
|
659
|
-
// During an active run both completed and running actions stay in liveRows —
|
|
660
|
-
// no stream events go to staticItems, which prevents <Static> growth and viewport jumps.
|
|
661
|
-
assert.equal(staticKeys.some((key) => key.includes("-action-1-")), false);
|
|
662
|
-
assert.equal(staticKeys.some((key) => key.includes("-action-2-")), false);
|
|
663
|
-
assert.ok(liveKeys.some((key) => key.includes("-action-1-")));
|
|
664
|
-
assert.ok(liveKeys.some((key) => key.includes("-action-2-")));
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
test("native transcript parts keep streaming response out of static rows", () => {
|
|
668
|
-
__clearTimelineMeasureCachesForTests();
|
|
669
|
-
|
|
670
|
-
const parts = buildNativeTranscriptParts(
|
|
671
|
-
[makeStreamingResponseRenderItem("Line one.\nLine two.\nLine three is arriving.")],
|
|
672
|
-
{ totalWidth: 72, debugLabel: "native-response-split" },
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
const staticText = snapshotText(parts.staticItems.flatMap((item) => item.rows));
|
|
676
|
-
const liveText = snapshotText(parts.liveRows);
|
|
677
|
-
|
|
678
|
-
assert.doesNotMatch(staticText, /Line three is arriving/);
|
|
679
|
-
assert.match(liveText, /Line three is arriving/);
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// ── Placement fix: running runs keep all events in liveRows ──────────────────
|
|
684
|
-
|
|
685
|
-
test("running run keeps all stream events in liveRows — no <Static> growth mid-generation", () => {
|
|
686
|
-
__clearTimelineMeasureCachesForTests();
|
|
687
|
-
|
|
688
|
-
const completedTool = makeTool({
|
|
689
|
-
id: "tool-1", command: "Get-Content README.md", status: "completed",
|
|
690
|
-
completedAt: 20, summary: "Read 5 lines", streamSeq: 1,
|
|
691
|
-
});
|
|
692
|
-
const runningTool = makeTool({
|
|
693
|
-
id: "tool-2", command: "Get-Content package.json", status: "running",
|
|
694
|
-
completedAt: null, summary: null, streamSeq: 2,
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
const thinkingBlock: RunProgressEntry["blocks"][number] = {
|
|
698
|
-
id: "block-1", text: "Let me inspect the files first.", status: "completed",
|
|
699
|
-
sequence: 1, createdAt: 1, updatedAt: 2,
|
|
700
|
-
};
|
|
701
|
-
const progressEntry: RunProgressEntry = {
|
|
702
|
-
id: "entry-1", source: "reasoning", text: thinkingBlock.text,
|
|
703
|
-
sequence: 1, createdAt: 1, updatedAt: 2, blocks: [thinkingBlock], pendingNewlineCount: 0,
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
const run: RunEvent = {
|
|
707
|
-
id: 50, type: "run", createdAt: 1, startedAt: 1, durationMs: null,
|
|
708
|
-
backendId: "codex-subprocess", backendLabel: "Codexa", runtime: TEST_RUNTIME,
|
|
709
|
-
prompt: "Inspect project",
|
|
710
|
-
progressEntries: [progressEntry],
|
|
711
|
-
status: "running", summary: "running...", truncatedOutput: false,
|
|
712
|
-
toolActivities: [completedTool, runningTool],
|
|
713
|
-
activity: [], touchedFileCount: 0, errorMessage: null, turnId: 5,
|
|
714
|
-
streamItems: [
|
|
715
|
-
{ kind: "thinking", streamSeq: 0, refId: "block-1" },
|
|
716
|
-
{ kind: "action", streamSeq: 1, refId: "tool-1" },
|
|
717
|
-
{ kind: "action", streamSeq: 2, refId: "tool-2" },
|
|
718
|
-
],
|
|
719
|
-
responseSegments: [], lastStreamSeq: 2, activeResponseSegmentId: null,
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
const user: UserPromptEvent = { id: 51, type: "user", createdAt: 1, prompt: "Inspect project", turnId: 5 };
|
|
723
|
-
const item: RenderTimelineItem = {
|
|
724
|
-
key: "turn-5",
|
|
725
|
-
type: "turn",
|
|
726
|
-
padded: true,
|
|
727
|
-
item: { type: "turn", turnId: 5, turnIndex: 0, user, run, assistant: null },
|
|
728
|
-
renderState: { opacity: "active", question: null, runPhase: "streaming" },
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
const parts = buildNativeTranscriptParts([item], { totalWidth: 80, debugLabel: "running-placement" });
|
|
732
|
-
|
|
733
|
-
// User prompt is always committed to staticItems immediately (correct behavior).
|
|
734
|
-
const staticKeys = parts.staticItems.flatMap((si) => si.rows.map((r) => r.key));
|
|
735
|
-
assert.ok(staticKeys.some((k) => k.includes("-user-")), "user row should be in staticItems");
|
|
736
|
-
|
|
737
|
-
// All stream events (thinking + completed action + running action) must be in liveRows —
|
|
738
|
-
// not in staticItems — while the run is active. This prevents <Static> from growing
|
|
739
|
-
// and causing viewport jumps during generation.
|
|
740
|
-
assert.equal(
|
|
741
|
-
parts.staticItems.filter((si) => si.key.includes("-stream-")).length,
|
|
742
|
-
0,
|
|
743
|
-
"no stream events should be in staticItems during an active run",
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
const liveKeys = parts.liveRows.map((r) => r.key);
|
|
747
|
-
assert.ok(liveKeys.some((k) => k.includes("-action-1-")), "completed action should be in liveRows");
|
|
748
|
-
assert.ok(liveKeys.some((k) => k.includes("-action-2-")), "running action should be in liveRows");
|
|
749
|
-
assert.ok(liveKeys.some((k) => k.includes("-codex-thinking-")), "thinking block should be in liveRows");
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
test("completed run moves all stream events to staticItems — one atomic commit after generation", () => {
|
|
753
|
-
__clearTimelineMeasureCachesForTests();
|
|
754
|
-
|
|
755
|
-
const tool1 = makeTool({
|
|
756
|
-
id: "tool-1", command: "Get-Content README.md", status: "completed",
|
|
757
|
-
completedAt: 20, summary: "Read 5 lines", streamSeq: 1,
|
|
758
|
-
});
|
|
759
|
-
const tool2 = makeTool({
|
|
760
|
-
id: "tool-2", command: "Get-Content package.json", status: "completed",
|
|
761
|
-
completedAt: 25, summary: "Read 10 lines", streamSeq: 2,
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
const thinkingBlock2: RunProgressEntry["blocks"][number] = {
|
|
765
|
-
id: "block-1", text: "Thinking done.", status: "completed",
|
|
766
|
-
sequence: 1, createdAt: 1, updatedAt: 2,
|
|
767
|
-
};
|
|
768
|
-
const progressEntry2: RunProgressEntry = {
|
|
769
|
-
id: "entry-1", source: "reasoning", text: thinkingBlock2.text,
|
|
770
|
-
sequence: 1, createdAt: 1, updatedAt: 2, blocks: [thinkingBlock2], pendingNewlineCount: 0,
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
const run: RunEvent = {
|
|
774
|
-
id: 52, type: "run", createdAt: 1, startedAt: 1, durationMs: 300,
|
|
775
|
-
backendId: "codex-subprocess", backendLabel: "Codexa", runtime: TEST_RUNTIME,
|
|
776
|
-
prompt: "Done project",
|
|
777
|
-
progressEntries: [progressEntry2],
|
|
778
|
-
status: "completed", summary: "completed", truncatedOutput: false,
|
|
779
|
-
toolActivities: [tool1, tool2],
|
|
780
|
-
activity: [], touchedFileCount: 0, errorMessage: null, turnId: 6,
|
|
781
|
-
streamItems: [
|
|
782
|
-
{ kind: "thinking", streamSeq: 0, refId: "block-1" },
|
|
783
|
-
{ kind: "action", streamSeq: 1, refId: "tool-1" },
|
|
784
|
-
{ kind: "action", streamSeq: 2, refId: "tool-2" },
|
|
785
|
-
],
|
|
786
|
-
responseSegments: [], lastStreamSeq: 2, activeResponseSegmentId: null,
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const user: UserPromptEvent = { id: 53, type: "user", createdAt: 1, prompt: "Done project", turnId: 6 };
|
|
790
|
-
const item: RenderTimelineItem = {
|
|
791
|
-
key: "turn-6",
|
|
792
|
-
type: "turn",
|
|
793
|
-
padded: true,
|
|
794
|
-
item: { type: "turn", turnId: 6, turnIndex: 0, user, run, assistant: null },
|
|
795
|
-
renderState: { opacity: "active", question: null, runPhase: "none" },
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const parts = buildNativeTranscriptParts([item], { totalWidth: 80, debugLabel: "completed-placement" });
|
|
799
|
-
|
|
800
|
-
// After run completes, all stream events must be in staticItems.
|
|
801
|
-
const staticItemKeys = parts.staticItems.map((si) => si.key);
|
|
802
|
-
assert.ok(staticItemKeys.some((k) => k.includes("-stream-1")), "action 1 should be in staticItems");
|
|
803
|
-
assert.ok(staticItemKeys.some((k) => k.includes("-stream-2")), "action 2 should be in staticItems");
|
|
804
|
-
|
|
805
|
-
// liveRows must be empty after run completes (user waits at prompt).
|
|
806
|
-
assert.equal(parts.liveRows.length, 0, "liveRows must be empty after run completes");
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
test("gap row keys use event.streamSeq — stable across compaction changes", () => {
|
|
810
|
-
__clearTimelineMeasureCachesForTests();
|
|
811
|
-
|
|
812
|
-
// Use non-sequential streamSeq values to distinguish from eventIndex (0,1,2).
|
|
813
|
-
const tools = [
|
|
814
|
-
makeTool({ id: "tool-1", command: "Get-Content a.txt", status: "completed", completedAt: 10, summary: "Read a", streamSeq: 3 }),
|
|
815
|
-
makeTool({ id: "tool-2", command: "Get-Content b.txt", status: "completed", completedAt: 11, summary: "Read b", streamSeq: 7 }),
|
|
816
|
-
makeTool({ id: "tool-3", command: "Get-Content c.txt", status: "completed", completedAt: 12, summary: "Read c", streamSeq: 12 }),
|
|
817
|
-
];
|
|
818
|
-
|
|
819
|
-
const run: RunEvent = {
|
|
820
|
-
id: 54, type: "run", createdAt: 1, startedAt: 1, durationMs: 200,
|
|
821
|
-
backendId: "codex-subprocess", backendLabel: "Codexa", runtime: TEST_RUNTIME,
|
|
822
|
-
prompt: "Read files",
|
|
823
|
-
progressEntries: [],
|
|
824
|
-
status: "completed", summary: "done", truncatedOutput: false,
|
|
825
|
-
toolActivities: tools,
|
|
826
|
-
activity: [], touchedFileCount: 0, errorMessage: null, turnId: 7,
|
|
827
|
-
streamItems: tools.map((t) => ({ kind: "action" as const, streamSeq: t.streamSeq!, refId: t.id })),
|
|
828
|
-
responseSegments: [], lastStreamSeq: 12, activeResponseSegmentId: null,
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
const user: UserPromptEvent = { id: 55, type: "user", createdAt: 1, prompt: "Read files", turnId: 7 };
|
|
832
|
-
const item: RenderTimelineItem = {
|
|
833
|
-
key: "turn-7",
|
|
834
|
-
type: "turn",
|
|
835
|
-
padded: true,
|
|
836
|
-
item: { type: "turn", turnId: 7, turnIndex: 0, user, run, assistant: null },
|
|
837
|
-
renderState: { opacity: "active", question: null, runPhase: "none" },
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
const parts = buildNativeTranscriptParts([item], { totalWidth: 80, debugLabel: "gap-key-test" });
|
|
841
|
-
const allRowKeys = parts.staticItems.flatMap((si) => si.rows.map((r) => r.key));
|
|
842
|
-
const gapKeys = allRowKeys.filter((k) => k.includes("-stream-gap-"));
|
|
843
|
-
|
|
844
|
-
// There should be gaps before tool-2 (streamSeq=7) and tool-3 (streamSeq=12).
|
|
845
|
-
// With the fix the gap key encodes the event's streamSeq, not the eventIndex.
|
|
846
|
-
assert.ok(gapKeys.some((k) => k.includes("-stream-gap-7")), "gap before tool-2 should use streamSeq=7");
|
|
847
|
-
assert.ok(gapKeys.some((k) => k.includes("-stream-gap-12")), "gap before tool-3 should use streamSeq=12");
|
|
848
|
-
// Old index-based keys (1, 2) must not be present.
|
|
849
|
-
assert.equal(gapKeys.some((k) => k.endsWith("-stream-gap-1")), false, "old eventIndex-based gap key must not exist");
|
|
850
|
-
assert.equal(gapKeys.some((k) => k.endsWith("-stream-gap-2")), false, "old eventIndex-based gap key must not exist");
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
test("timeline measurement coverage for THINKING -> RESPONDING -> FINALIZE_RUN", () => {
|
|
854
|
-
__clearTimelineMeasureCachesForTests();
|
|
855
|
-
|
|
856
|
-
const tool = makeTool({ id: "tool-1", status: "running", completedAt: null, summary: null, streamSeq: 1 });
|
|
857
|
-
const runEvent: RunEvent = {
|
|
858
|
-
id: 2,
|
|
859
|
-
type: "run",
|
|
860
|
-
createdAt: 2,
|
|
861
|
-
startedAt: 2,
|
|
862
|
-
durationMs: null,
|
|
863
|
-
backendId: "codex-subprocess",
|
|
864
|
-
backendLabel: "Test",
|
|
865
|
-
runtime: TEST_RUNTIME,
|
|
866
|
-
prompt: "Test",
|
|
867
|
-
progressEntries: [],
|
|
868
|
-
status: "running",
|
|
869
|
-
summary: "Running",
|
|
870
|
-
truncatedOutput: false,
|
|
871
|
-
toolActivities: [tool],
|
|
872
|
-
activity: [],
|
|
873
|
-
touchedFileCount: 0,
|
|
874
|
-
errorMessage: null,
|
|
875
|
-
turnId: 1,
|
|
876
|
-
streamItems: [{ kind: "action", streamSeq: 1, refId: "tool-1" }],
|
|
877
|
-
responseSegments: [],
|
|
878
|
-
lastStreamSeq: 1,
|
|
879
|
-
activeResponseSegmentId: null,
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
const item1: RenderTimelineItem = {
|
|
883
|
-
key: "turn-1",
|
|
884
|
-
type: "turn",
|
|
885
|
-
padded: true,
|
|
886
|
-
item: { type: "turn", turnId: 1, turnIndex: 1, user: null, run: runEvent, assistant: null },
|
|
887
|
-
renderState: { opacity: "active", question: null, runPhase: "thinking" },
|
|
888
|
-
};
|
|
889
|
-
|
|
890
|
-
const snapshot1 = buildTimelineSnapshot([item1], { totalWidth: 120 });
|
|
891
|
-
const actionKeys1 = snapshot1.rows.filter(r => r.key.includes("-action-")).map(r => r.key);
|
|
892
|
-
assert.ok(actionKeys1.length > 0);
|
|
893
|
-
|
|
894
|
-
// Complete action
|
|
895
|
-
tool.status = "completed";
|
|
896
|
-
const snapshot2 = buildTimelineSnapshot([item1], { totalWidth: 120 });
|
|
897
|
-
const actionKeys2 = snapshot2.rows.filter(r => r.key.includes("-action-")).map(r => r.key);
|
|
898
|
-
assert.deepEqual(actionKeys2, actionKeys1);
|
|
899
|
-
|
|
900
|
-
// Add response
|
|
901
|
-
runEvent.responseSegments = [{
|
|
902
|
-
id: "resp-1",
|
|
903
|
-
streamSeq: 2,
|
|
904
|
-
chunks: ["Answer starts"],
|
|
905
|
-
status: "active",
|
|
906
|
-
startedAt: 3,
|
|
907
|
-
}];
|
|
908
|
-
runEvent.streamItems = [...(runEvent.streamItems ?? []), { kind: "response", streamSeq: 2, refId: "resp-1" }];
|
|
909
|
-
item1.renderState.runPhase = "streaming";
|
|
910
|
-
|
|
911
|
-
const snapshot3 = buildTimelineSnapshot([item1], { totalWidth: 120 });
|
|
912
|
-
const actionKeys3 = snapshot3.rows.filter(r => r.key.includes("-action-")).map(r => r.key);
|
|
913
|
-
assert.deepEqual(actionKeys3, actionKeys1);
|
|
914
|
-
|
|
915
|
-
// Finalize
|
|
916
|
-
runEvent.status = "completed";
|
|
917
|
-
runEvent.durationMs = 100;
|
|
918
|
-
runEvent.responseSegments[0].status = "completed";
|
|
919
|
-
item1.renderState.runPhase = "none";
|
|
920
|
-
|
|
921
|
-
const snapshot4 = buildTimelineSnapshot([item1], { totalWidth: 120 });
|
|
922
|
-
const actionKeys4 = snapshot4.rows.filter(r => r.key.includes("-action-")).map(r => r.key);
|
|
923
|
-
assert.deepEqual(actionKeys4, actionKeys1);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
// ─── wrapStyledSpans word-boundary regression tests ───────────────────────────
|
|
927
|
-
|
|
928
|
-
function assertNoMidWordSplit(rowTexts: string[], words: string[]) {
|
|
929
|
-
for (const word of words) {
|
|
930
|
-
for (let r = 0; r < rowTexts.length - 1; r++) {
|
|
931
|
-
const tail = rowTexts[r]!;
|
|
932
|
-
const head = rowTexts[r + 1]!;
|
|
933
|
-
for (let split = 1; split < word.length; split++) {
|
|
934
|
-
const prefix = word.slice(0, split);
|
|
935
|
-
const suffix = word.slice(split);
|
|
936
|
-
assert.ok(
|
|
937
|
-
!(tail.endsWith(prefix) && head.startsWith(suffix)),
|
|
938
|
-
`"${word}" split as "${prefix}" | "${suffix}" between rows ${r} and ${r + 1}`,
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
test("wrapStyledSpans: no mid-word split within a single styled span", () => {
|
|
946
|
-
const spans: TimelineRowSpan[] = [
|
|
947
|
-
{ text: "where the test mock for stdout is missing some WriteStream properties.", tone: "info" },
|
|
948
|
-
];
|
|
949
|
-
const rows = __wrapStyledSpansForTests(spans, 40);
|
|
950
|
-
const rowTexts = rows.map((row) => row.map((s) => s.text).join(""));
|
|
951
|
-
assertNoMidWordSplit(rowTexts, ["for", "stdout", "WriteStream", "where", "test", "mock", "missing", "some", "properties"]);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
test("wrapStyledSpans: no mid-word split across mixed styled spans", () => {
|
|
955
|
-
const spans: TimelineRowSpan[] = [
|
|
956
|
-
{ text: "normal text before ", tone: undefined },
|
|
957
|
-
{ text: "src/ui/ActivityIndicator.test.tsx", tone: "info" },
|
|
958
|
-
{ text: " where the test mock for", tone: undefined },
|
|
959
|
-
{ text: " WriteStream", tone: "info" },
|
|
960
|
-
];
|
|
961
|
-
const rows = __wrapStyledSpansForTests(spans, 40);
|
|
962
|
-
const rowTexts = rows.map((row) => row.map((s) => s.text).join(""));
|
|
963
|
-
assertNoMidWordSplit(rowTexts, ["for", "WriteStream", "where", "mock", "normal", "text", "before"]);
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
test("wrapStyledSpans: overlong token falls back to character split within line width", () => {
|
|
967
|
-
const spans: TimelineRowSpan[] = [
|
|
968
|
-
{ text: "a_very_long_token_without_spaces_that_exceeds_width", tone: "muted" },
|
|
969
|
-
];
|
|
970
|
-
const rows = __wrapStyledSpansForTests(spans, 20);
|
|
971
|
-
assert.ok(rows.length > 1, "should produce multiple rows for an overlong token");
|
|
972
|
-
for (const row of rows) {
|
|
973
|
-
const rowWidth = row.reduce((sum, s) => sum + s.text.length, 0);
|
|
974
|
-
assert.ok(rowWidth <= 20, `row width ${rowWidth} exceeds 20`);
|
|
975
|
-
}
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
test("wrapStyledSpans: hard newlines in span text produce separate rows", () => {
|
|
979
|
-
const spans: TimelineRowSpan[] = [
|
|
980
|
-
{ text: "first line\nsecond line", tone: "text" as never },
|
|
981
|
-
];
|
|
982
|
-
const rows = __wrapStyledSpansForTests(spans, 80);
|
|
983
|
-
assert.equal(rows.length, 2);
|
|
984
|
-
assert.ok(rows[0]!.map((s) => s.text).join("").includes("first line"));
|
|
985
|
-
assert.ok(rows[1]!.map((s) => s.text).join("").includes("second line"));
|
|
986
|
-
});
|