@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,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
|
|
4
|
+
import type { RunEvent, TimelineEvent } from "./types.js";
|
|
5
|
+
import { hasFinalizedTranscriptPlan } from "./planTranscript.js";
|
|
6
|
+
|
|
7
|
+
function makeRun(overrides: Partial<RunEvent> = {}): RunEvent {
|
|
8
|
+
return {
|
|
9
|
+
id: 1,
|
|
10
|
+
type: "run",
|
|
11
|
+
createdAt: 1,
|
|
12
|
+
startedAt: 1,
|
|
13
|
+
durationMs: 10,
|
|
14
|
+
backendId: "codex-subprocess",
|
|
15
|
+
backendLabel: "Codexa",
|
|
16
|
+
runtime: TEST_RUNTIME,
|
|
17
|
+
prompt: "Plan work",
|
|
18
|
+
progressEntries: [],
|
|
19
|
+
status: "completed",
|
|
20
|
+
summary: "completed",
|
|
21
|
+
truncatedOutput: false,
|
|
22
|
+
toolActivities: [],
|
|
23
|
+
activity: [],
|
|
24
|
+
touchedFileCount: 0,
|
|
25
|
+
errorMessage: null,
|
|
26
|
+
turnId: 1,
|
|
27
|
+
streamItems: [{ kind: "plan", streamSeq: 1, refId: "plan-1" }],
|
|
28
|
+
responseSegments: [],
|
|
29
|
+
lastStreamSeq: 1,
|
|
30
|
+
activeResponseSegmentId: null,
|
|
31
|
+
plan: {
|
|
32
|
+
id: "plan-1",
|
|
33
|
+
streamSeq: 1,
|
|
34
|
+
chunks: ["1. Inspect\n2. Update"],
|
|
35
|
+
status: "completed",
|
|
36
|
+
startedAt: 1,
|
|
37
|
+
},
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("approval visibility requires a non-empty finalized transcript plan", () => {
|
|
43
|
+
assert.equal(hasFinalizedTranscriptPlan([], "1. Inspect"), false);
|
|
44
|
+
assert.equal(hasFinalizedTranscriptPlan([makeRun()], ""), false);
|
|
45
|
+
assert.equal(hasFinalizedTranscriptPlan([makeRun({ plan: null })], "1. Inspect"), false);
|
|
46
|
+
assert.equal(
|
|
47
|
+
hasFinalizedTranscriptPlan([
|
|
48
|
+
makeRun({
|
|
49
|
+
status: "running",
|
|
50
|
+
plan: {
|
|
51
|
+
id: "plan-1",
|
|
52
|
+
streamSeq: 1,
|
|
53
|
+
chunks: ["1. Inspect"],
|
|
54
|
+
status: "active",
|
|
55
|
+
startedAt: 1,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
], "1. Inspect"),
|
|
59
|
+
false,
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
hasFinalizedTranscriptPlan([makeRun()] satisfies TimelineEvent[], "1. Inspect\n2. Update"),
|
|
63
|
+
true,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TimelineEvent } from "./types.js";
|
|
2
|
+
import { getRunPlanText } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function hasFinalizedTranscriptPlan(events: readonly TimelineEvent[], planText: string | null | undefined): boolean {
|
|
5
|
+
const expected = planText?.trim() ?? "";
|
|
6
|
+
if (!expected) return false;
|
|
7
|
+
|
|
8
|
+
return events.some((event) => {
|
|
9
|
+
if (event.type !== "run") return false;
|
|
10
|
+
if (event.status !== "completed") return false;
|
|
11
|
+
if (event.plan?.status !== "completed") return false;
|
|
12
|
+
const transcriptPlan = getRunPlanText(event.plan).trim();
|
|
13
|
+
return transcriptPlan.length > 0 && transcriptPlan === expected;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { schedulePromptRunStartAfterVisibleCommit } from "./promptRunSchedule.js";
|
|
4
|
+
|
|
5
|
+
function waitForTimerTurn(): Promise<void> {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test("schedulePromptRunStartAfterVisibleCommit waits past the reducer microtask before starting work", async () => {
|
|
10
|
+
const order: string[] = [];
|
|
11
|
+
|
|
12
|
+
schedulePromptRunStartAfterVisibleCommit(() => {
|
|
13
|
+
order.push("provider-started");
|
|
14
|
+
});
|
|
15
|
+
order.push("visible-run-dispatched");
|
|
16
|
+
|
|
17
|
+
await Promise.resolve();
|
|
18
|
+
assert.deepEqual(order, ["visible-run-dispatched"]);
|
|
19
|
+
|
|
20
|
+
await waitForTimerTurn();
|
|
21
|
+
assert.deepEqual(order, ["visible-run-dispatched", "provider-started"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("schedulePromptRunStartAfterVisibleCommit can be canceled before provider start", async () => {
|
|
25
|
+
let started = false;
|
|
26
|
+
|
|
27
|
+
const cancel = schedulePromptRunStartAfterVisibleCommit(() => {
|
|
28
|
+
started = true;
|
|
29
|
+
});
|
|
30
|
+
cancel();
|
|
31
|
+
|
|
32
|
+
await Promise.resolve();
|
|
33
|
+
await waitForTimerTurn();
|
|
34
|
+
|
|
35
|
+
assert.equal(started, false);
|
|
36
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CancelScheduledPromptRunStart = () => void;
|
|
2
|
+
|
|
3
|
+
export function schedulePromptRunStartAfterVisibleCommit(
|
|
4
|
+
start: () => void,
|
|
5
|
+
): CancelScheduledPromptRunStart {
|
|
6
|
+
let cancelled = false;
|
|
7
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
8
|
+
|
|
9
|
+
queueMicrotask(() => {
|
|
10
|
+
if (cancelled) return;
|
|
11
|
+
timer = setTimeout(() => {
|
|
12
|
+
timer = null;
|
|
13
|
+
if (!cancelled) {
|
|
14
|
+
start();
|
|
15
|
+
}
|
|
16
|
+
}, 0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
cancelled = true;
|
|
21
|
+
if (timer) {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
timer = null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthPreference,
|
|
3
|
+
AvailableBackend,
|
|
4
|
+
} from "../config/settings.js";
|
|
5
|
+
import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
|
|
6
|
+
import type { RunActivitySummary, RunFileActivity } from "../core/workspaceActivity.js";
|
|
7
|
+
|
|
8
|
+
// ─── Screen routing ──────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type Screen =
|
|
11
|
+
| "main"
|
|
12
|
+
| "model-picker"
|
|
13
|
+
| "mode-picker"
|
|
14
|
+
| "backend-picker"
|
|
15
|
+
| "provider-picker"
|
|
16
|
+
| "auth-panel"
|
|
17
|
+
| "reasoning-picker"
|
|
18
|
+
| "theme-picker"
|
|
19
|
+
| "settings-panel"
|
|
20
|
+
| "permissions-panel"
|
|
21
|
+
| "permissions-approval-picker"
|
|
22
|
+
| "permissions-sandbox-picker"
|
|
23
|
+
| "permissions-network-picker"
|
|
24
|
+
| "permissions-add-writable-root"
|
|
25
|
+
| "permissions-remove-writable-root"
|
|
26
|
+
| "import-confirmation";
|
|
27
|
+
|
|
28
|
+
// ─── Provider readiness ───────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export type ExternalCliStatus = "idle" | "starting" | "ready" | "failed";
|
|
31
|
+
|
|
32
|
+
// ─── UI State Machine ─────────────────────────────────────────────────────────
|
|
33
|
+
// Drives all visual decisions: border colors, input persona, turn opacity.
|
|
34
|
+
//
|
|
35
|
+
// IDLE ──submit──▶ THINKING ──first token──▶ RESPONDING ──answer visible──▶ ANSWER_VISIBLE ──complete──▶ IDLE
|
|
36
|
+
// ▲ │ │ │
|
|
37
|
+
// │ └──error──▶ ERROR └──question──▶ AWAITING
|
|
38
|
+
// └──────────────────────────────────────── dismiss / answer ──────────┘
|
|
39
|
+
|
|
40
|
+
export type UIState =
|
|
41
|
+
| { kind: "IDLE" }
|
|
42
|
+
| { kind: "THINKING"; turnId: number }
|
|
43
|
+
| { kind: "RESPONDING"; turnId: number }
|
|
44
|
+
| { kind: "ANSWER_VISIBLE"; turnId: number }
|
|
45
|
+
| { kind: "AWAITING_USER_ACTION"; turnId: number; question: string }
|
|
46
|
+
| { kind: "ERROR"; turnId: number; message: string }
|
|
47
|
+
| { kind: "SHELL_RUNNING"; shellId: number };
|
|
48
|
+
|
|
49
|
+
/** Derive the legacy busy flag from UIState for guard functions. */
|
|
50
|
+
export function isBusy(state: UIState): boolean {
|
|
51
|
+
return state.kind === "THINKING"
|
|
52
|
+
|| state.kind === "RESPONDING"
|
|
53
|
+
|| state.kind === "ANSWER_VISIBLE"
|
|
54
|
+
|| state.kind === "SHELL_RUNNING";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Timeline events ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export interface TimelineBaseEvent {
|
|
60
|
+
id: number;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UserPromptEvent extends TimelineBaseEvent {
|
|
65
|
+
type: "user";
|
|
66
|
+
prompt: string;
|
|
67
|
+
/** Links this user prompt to its run and assistant response in a TurnGroup. */
|
|
68
|
+
turnId: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AssistantEvent extends TimelineBaseEvent {
|
|
72
|
+
type: "assistant";
|
|
73
|
+
content: string;
|
|
74
|
+
/** Accumulated chunks during streaming — avoids O(n²) string concatenation. */
|
|
75
|
+
contentChunks: string[];
|
|
76
|
+
/** Links this response back to the originating user prompt + run. */
|
|
77
|
+
turnId: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the full assistant content string. During streaming, joins the
|
|
82
|
+
* accumulated chunks (single O(n) allocation). After finalization, returns
|
|
83
|
+
* the pre-joined `content` field directly.
|
|
84
|
+
*/
|
|
85
|
+
export function getAssistantContent(event: AssistantEvent | null | undefined): string {
|
|
86
|
+
if (!event) return "";
|
|
87
|
+
if (event.contentChunks.length > 0) return event.contentChunks.join("");
|
|
88
|
+
return event.content;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getResponseSegmentText(segment: RunResponseSegment | null | undefined): string {
|
|
92
|
+
if (!segment) return "";
|
|
93
|
+
return segment.chunks.join("");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RunToolActivity {
|
|
97
|
+
id: string;
|
|
98
|
+
command: string;
|
|
99
|
+
status: "running" | "completed" | "failed";
|
|
100
|
+
startedAt: number;
|
|
101
|
+
completedAt?: number | null;
|
|
102
|
+
summary?: string | null;
|
|
103
|
+
/** Turn-global ordering against thinking blocks and response segments. */
|
|
104
|
+
streamSeq?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type RunProgressSource =
|
|
108
|
+
| "reasoning"
|
|
109
|
+
| "todo"
|
|
110
|
+
| "stdout"
|
|
111
|
+
| "tool"
|
|
112
|
+
| "activity"
|
|
113
|
+
| "stderr"
|
|
114
|
+
| "transcript";
|
|
115
|
+
|
|
116
|
+
export interface RunProgressBlock {
|
|
117
|
+
id: string;
|
|
118
|
+
text: string;
|
|
119
|
+
sequence: number;
|
|
120
|
+
createdAt: number;
|
|
121
|
+
updatedAt: number;
|
|
122
|
+
status: "active" | "completed";
|
|
123
|
+
/** Turn-global ordering against tools and response segments. Set when block first becomes visible. */
|
|
124
|
+
streamSeq?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RunResponseSegment {
|
|
128
|
+
id: string;
|
|
129
|
+
streamSeq: number;
|
|
130
|
+
chunks: string[];
|
|
131
|
+
status: "active" | "completed";
|
|
132
|
+
startedAt: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface RunPlanBlock {
|
|
136
|
+
id: string;
|
|
137
|
+
streamSeq: number;
|
|
138
|
+
chunks: string[];
|
|
139
|
+
status: "active" | "completed";
|
|
140
|
+
startedAt: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getRunPlanText(plan: RunPlanBlock | null | undefined): string {
|
|
144
|
+
if (!plan) return "";
|
|
145
|
+
return plan.chunks.join("");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface RunStreamItem {
|
|
149
|
+
streamSeq: number;
|
|
150
|
+
kind: "thinking" | "action" | "response" | "plan";
|
|
151
|
+
/** block.id, tool.id, response-segment id, or plan.id depending on kind. */
|
|
152
|
+
refId: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface RunProgressEntry {
|
|
156
|
+
id: string;
|
|
157
|
+
source: RunProgressSource;
|
|
158
|
+
/** Latest normalized full text received for this upstream progress item. */
|
|
159
|
+
text: string;
|
|
160
|
+
sequence: number;
|
|
161
|
+
createdAt: number;
|
|
162
|
+
updatedAt: number;
|
|
163
|
+
/** Structured visible blocks derived from `text` with stable identities. */
|
|
164
|
+
blocks: RunProgressBlock[];
|
|
165
|
+
/**
|
|
166
|
+
* Count of trailing newlines that have not yet been committed into a block.
|
|
167
|
+
* This keeps single-newline vs double-newline boundaries streaming-safe.
|
|
168
|
+
*/
|
|
169
|
+
pendingNewlineCount: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SystemEvent extends TimelineBaseEvent {
|
|
173
|
+
type: "system";
|
|
174
|
+
title: string;
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ErrorEvent extends TimelineBaseEvent {
|
|
179
|
+
type: "error";
|
|
180
|
+
title: string;
|
|
181
|
+
content: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RunEvent extends TimelineBaseEvent {
|
|
185
|
+
type: "run";
|
|
186
|
+
backendId: AvailableBackend;
|
|
187
|
+
backendLabel: string;
|
|
188
|
+
runtime: ResolvedRuntimeConfig;
|
|
189
|
+
prompt: string;
|
|
190
|
+
progressEntries: RunProgressEntry[];
|
|
191
|
+
status: "running" | "completed" | "failed" | "canceled";
|
|
192
|
+
summary: string;
|
|
193
|
+
truncatedOutput: boolean;
|
|
194
|
+
startedAt: number;
|
|
195
|
+
durationMs: number | null;
|
|
196
|
+
toolActivities: RunToolActivity[];
|
|
197
|
+
activity: RunFileActivity[];
|
|
198
|
+
touchedFileCount: number;
|
|
199
|
+
activitySummary?: RunActivitySummary;
|
|
200
|
+
errorMessage?: string | null;
|
|
201
|
+
/** Links this run to its user prompt for TurnGroup rendering. */
|
|
202
|
+
turnId: number;
|
|
203
|
+
/** Chronologically ordered references to thinking/action/response items. */
|
|
204
|
+
streamItems?: RunStreamItem[];
|
|
205
|
+
/** Discrete response segments — interrupted by thinking/action events. */
|
|
206
|
+
responseSegments?: RunResponseSegment[];
|
|
207
|
+
/** Monotonic counter the reducer uses to assign streamSeq across all events. */
|
|
208
|
+
lastStreamSeq?: number;
|
|
209
|
+
/** When set, the next assistant delta extends this segment; cleared by thinking/action. */
|
|
210
|
+
activeResponseSegmentId?: string | null;
|
|
211
|
+
/** First-class plan content rendered as a stable stream item. */
|
|
212
|
+
plan?: RunPlanBlock | null;
|
|
213
|
+
/** The approved plan text, injected at the start of execution turns. */
|
|
214
|
+
approvedPlan?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ShellEvent extends TimelineBaseEvent {
|
|
218
|
+
type: "shell";
|
|
219
|
+
command: string;
|
|
220
|
+
lines: string[];
|
|
221
|
+
stderrLines: string[];
|
|
222
|
+
summary?: string | null;
|
|
223
|
+
status: "running" | "completed" | "failed";
|
|
224
|
+
exitCode: number | null;
|
|
225
|
+
durationMs: number | null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type TimelineEvent = UserPromptEvent | AssistantEvent | SystemEvent | ErrorEvent | RunEvent | ShellEvent;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolveRuntimeConfig, type RuntimeConfig, DEFAULT_RUNTIME_CONFIG, type ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
|
|
2
|
+
|
|
3
|
+
export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): ResolvedRuntimeConfig {
|
|
4
|
+
return resolveRuntimeConfig({
|
|
5
|
+
...DEFAULT_RUNTIME_CONFIG,
|
|
6
|
+
...overrides,
|
|
7
|
+
policy: {
|
|
8
|
+
...DEFAULT_RUNTIME_CONFIG.policy,
|
|
9
|
+
...overrides.policy,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TEST_RUNTIME = makeResolvedRuntime();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { getUsableShellWidth } from "./layout.js";
|
|
4
|
+
import { wrapPlainText } from "./textLayout.js";
|
|
5
|
+
import { useTheme } from "./theme.js";
|
|
6
|
+
|
|
7
|
+
interface ActionRequiredBlockProps {
|
|
8
|
+
cols: number;
|
|
9
|
+
turnIndex: number;
|
|
10
|
+
question: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ActionRequiredBlock({ cols, turnIndex, question }: ActionRequiredBlockProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
const contentWidth = Math.max(1, getUsableShellWidth(cols, 6));
|
|
16
|
+
|
|
17
|
+
const wrappedContent = question
|
|
18
|
+
.split("\n")
|
|
19
|
+
.flatMap((line) => {
|
|
20
|
+
const rows = wrapPlainText(line, contentWidth);
|
|
21
|
+
return rows.length > 0 ? rows : [""];
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box borderStyle="single" borderColor={theme.BORDER_ACTIVE} flexDirection="column" marginBottom={1} width="100%">
|
|
26
|
+
<Box width="100%" justifyContent="space-between" overflow="hidden" paddingX={1}>
|
|
27
|
+
<Text color={theme.TEXT} bold>{`[${turnIndex}] ACTION REQUIRED`}</Text>
|
|
28
|
+
<Text color={theme.TEXT} bold>{"⚡"}</Text>
|
|
29
|
+
</Box>
|
|
30
|
+
<Box flexDirection="column" paddingX={2} marginTop={1} marginBottom={1} width="100%">
|
|
31
|
+
<Text color={theme.TEXT} bold>{"Verification Question"}</Text>
|
|
32
|
+
{wrappedContent.map((row, index) => (
|
|
33
|
+
<Text key={`content-${index}`} color={theme.TEXT}>{row || " "}</Text>
|
|
34
|
+
))}
|
|
35
|
+
</Box>
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useTheme } from "./theme.js";
|
|
4
|
+
|
|
5
|
+
// ─── WaveBar ─────────────────────────────────────────────────────────────────
|
|
6
|
+
// Audio-visualizer style wave. Renders as a single <Text> node to minimise
|
|
7
|
+
// the React element tree — important for keeping Ink re-renders cheap.
|
|
8
|
+
|
|
9
|
+
const WAVE_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const;
|
|
10
|
+
const WAVE_COLS = 12;
|
|
11
|
+
const WAVE_SPEED = 0.38; // radians per tick
|
|
12
|
+
const WAVE_SPREAD = 0.6; // phase offset between adjacent columns
|
|
13
|
+
|
|
14
|
+
interface WaveBarProps {
|
|
15
|
+
tick: number;
|
|
16
|
+
color?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function WaveBar({ tick, color }: WaveBarProps) {
|
|
20
|
+
const theme = useTheme();
|
|
21
|
+
const activeColor = color ?? theme.ACCENT;
|
|
22
|
+
|
|
23
|
+
let str = "";
|
|
24
|
+
for (let c = 0; c < WAVE_COLS; c++) {
|
|
25
|
+
const phase = tick * WAVE_SPEED + c * WAVE_SPREAD;
|
|
26
|
+
const normalized = (Math.sin(phase) + 1) / 2; // [0, 1]
|
|
27
|
+
const idx = Math.round(normalized * (WAVE_CHARS.length - 1));
|
|
28
|
+
str += WAVE_CHARS[idx] ?? "▄";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <Text color={activeColor}>{str}</Text>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── PulseBar ─────────────────────────────────────────────────────────────────
|
|
35
|
+
// Indeterminate progress bar. Renders as 3 <Text> nodes (before / fill / after).
|
|
36
|
+
|
|
37
|
+
const PULSE_TOTAL = 20;
|
|
38
|
+
const PULSE_FILL = 6;
|
|
39
|
+
const PULSE_MAX_POS = PULSE_TOTAL - PULSE_FILL; // 14
|
|
40
|
+
const PULSE_PERIOD = PULSE_MAX_POS * 2; // 28
|
|
41
|
+
const PULSE_SPEED = 2; // ticks per position step
|
|
42
|
+
|
|
43
|
+
interface PulseBarProps {
|
|
44
|
+
tick: number;
|
|
45
|
+
color?: string;
|
|
46
|
+
dimColor?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function PulseBar({ tick, color, dimColor }: PulseBarProps) {
|
|
50
|
+
const theme = useTheme();
|
|
51
|
+
const activeColor = color ?? theme.ACCENT;
|
|
52
|
+
const inactiveColor = dimColor ?? theme.DIM;
|
|
53
|
+
|
|
54
|
+
const raw = Math.floor(tick / PULSE_SPEED) % PULSE_PERIOD;
|
|
55
|
+
const pos = raw < PULSE_MAX_POS ? raw : PULSE_PERIOD - raw;
|
|
56
|
+
|
|
57
|
+
const before = "░".repeat(pos);
|
|
58
|
+
const fill = "█".repeat(PULSE_FILL);
|
|
59
|
+
const after = "░".repeat(PULSE_TOTAL - pos - PULSE_FILL);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Box>
|
|
63
|
+
<Text color={inactiveColor}>{before}</Text>
|
|
64
|
+
<Text color={activeColor}>{fill}</Text>
|
|
65
|
+
<Text color={inactiveColor}>{after}</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { PassThrough } from "node:stream";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
import { ActivityIndicator } from "./ActivityIndicator.js";
|
|
7
|
+
|
|
8
|
+
class TestOutput extends PassThrough {
|
|
9
|
+
readonly isTTY = true;
|
|
10
|
+
columns = 120;
|
|
11
|
+
rows = 40;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderIndicator(props: any): Promise<string> {
|
|
15
|
+
const stdout = new TestOutput();
|
|
16
|
+
let output = "";
|
|
17
|
+
stdout.on("data", (chunk) => {
|
|
18
|
+
output += chunk.toString();
|
|
19
|
+
});
|
|
20
|
+
const instance = render(<ActivityIndicator {...props} />, { stdout: stdout as unknown as NodeJS.WriteStream });
|
|
21
|
+
instance.unmount();
|
|
22
|
+
return Promise.resolve(output);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("idle state renders static subtle indicator", async () => {
|
|
26
|
+
const output = await renderIndicator({ uiState: { kind: "IDLE" }, externalCliStatus: "idle" });
|
|
27
|
+
assert.ok(output.includes("?"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("error state renders error indicator", async () => {
|
|
31
|
+
const output = await renderIndicator({ uiState: { kind: "ERROR", turnId: 1, message: "err" } });
|
|
32
|
+
assert.ok(output.includes("�"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("tool/action state renders action indicator", async () => {
|
|
36
|
+
const output = await renderIndicator({ uiState: { kind: "SHELL_RUNNING", shellId: 1 } });
|
|
37
|
+
assert.ok(output.includes("?"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("waiting/thinking state renders animated indicator frame", async () => {
|
|
41
|
+
const output = await renderIndicator({ uiState: { kind: "THINKING", turnId: 1 } });
|
|
42
|
+
assert.ok(output.includes("?"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("streaming state renders streaming indicator frame", async () => {
|
|
46
|
+
const output = await renderIndicator({ uiState: { kind: "RESPONDING", turnId: 1 } });
|
|
47
|
+
assert.ok(output.includes("?")); // first frame
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("provider loading renders animated indicator frame", async () => {
|
|
51
|
+
const output = await renderIndicator({ uiState: { kind: "IDLE" }, externalCliStatus: "starting" });
|
|
52
|
+
assert.ok(output.includes("?"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("provider failed renders error indicator", async () => {
|
|
56
|
+
const output = await renderIndicator({ uiState: { kind: "IDLE" }, externalCliStatus: "failed" });
|
|
57
|
+
assert.ok(output.includes("�"));
|
|
58
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import type { UIState, ExternalCliStatus } from "../session/types.js";
|
|
4
|
+
import { useTheme } from "./theme.js";
|
|
5
|
+
|
|
6
|
+
const SPINNER_FRAMES = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
|
|
7
|
+
const STREAMING_FRAMES = ["?", "?", "?", "?"];
|
|
8
|
+
|
|
9
|
+
export interface ActivityIndicatorProps {
|
|
10
|
+
uiState: UIState;
|
|
11
|
+
externalCliStatus?: ExternalCliStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: ActivityIndicatorProps) {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
17
|
+
|
|
18
|
+
const isError = uiState.kind === "ERROR" || externalCliStatus === "failed";
|
|
19
|
+
const isStarting = externalCliStatus === "starting";
|
|
20
|
+
const isThinking = uiState.kind === "THINKING";
|
|
21
|
+
const isStreaming = uiState.kind === "RESPONDING";
|
|
22
|
+
const isAction = uiState.kind === "SHELL_RUNNING";
|
|
23
|
+
|
|
24
|
+
const isAnimated = isStarting || isThinking || isStreaming;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isAnimated) return;
|
|
28
|
+
const interval = setInterval(() => {
|
|
29
|
+
setFrameIndex((prev) => prev + 1);
|
|
30
|
+
}, 80);
|
|
31
|
+
return () => clearInterval(interval);
|
|
32
|
+
}, [isAnimated]);
|
|
33
|
+
|
|
34
|
+
let glyph = "?";
|
|
35
|
+
let color = theme.DIM;
|
|
36
|
+
let bold = false;
|
|
37
|
+
|
|
38
|
+
if (isError) {
|
|
39
|
+
glyph = "�";
|
|
40
|
+
color = theme.ERROR;
|
|
41
|
+
bold = true;
|
|
42
|
+
} else if (isAction) {
|
|
43
|
+
glyph = "?";
|
|
44
|
+
color = theme.WARNING;
|
|
45
|
+
bold = true;
|
|
46
|
+
} else if (isStarting || isThinking) {
|
|
47
|
+
glyph = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]!;
|
|
48
|
+
color = theme.TEXT;
|
|
49
|
+
} else if (isStreaming) {
|
|
50
|
+
glyph = STREAMING_FRAMES[frameIndex % STREAMING_FRAMES.length]!;
|
|
51
|
+
color = theme.SUCCESS;
|
|
52
|
+
} else {
|
|
53
|
+
glyph = "?";
|
|
54
|
+
color = theme.DIM;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return <Text color={color} bold={bold}>{glyph}</Text>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
|
|
3
|
+
test("AgentBlock output streams organically without layout truncation", () => {
|
|
4
|
+
// buildStreamingPreviewRows has been intentionally removed to support
|
|
5
|
+
// true variable-height scrolling transcript behavior per project requirements.
|
|
6
|
+
});
|