@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
package/src/ui/layout.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive layout constants and hook.
|
|
3
|
+
*
|
|
4
|
+
* Three modes based purely on terminal column count:
|
|
5
|
+
*
|
|
6
|
+
* full ≥ 110 cols → full banner, side-by-side header info
|
|
7
|
+
* compact 60–109 → single-line mini logo, stacked info, condensed composer
|
|
8
|
+
* micro < 60 → no logo, one-line header, ultra-compact composer
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useRef, useState } from "react";
|
|
12
|
+
import { useStdout } from "ink";
|
|
13
|
+
import stringWidth from "string-width";
|
|
14
|
+
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
15
|
+
|
|
16
|
+
export const BREAKPOINT_FULL = 110; // ≥ this → full
|
|
17
|
+
export const BREAKPOINT_COMPACT = 60; // ≥ this → compact; below → micro
|
|
18
|
+
export const MIN_VIEWPORT_COLS = 20;
|
|
19
|
+
export const MIN_VIEWPORT_ROWS = 10;
|
|
20
|
+
export const RESTORE_SETTLE_MS = 100;
|
|
21
|
+
export const STARTUP_TINY_MIN_COLS = 40;
|
|
22
|
+
export const STARTUP_TINY_MIN_ROWS = 14;
|
|
23
|
+
export const STARTUP_FULL_MIN_COLS = BREAKPOINT_FULL;
|
|
24
|
+
export const STARTUP_FULL_MIN_BODY_ROWS = 4;
|
|
25
|
+
export const STARTUP_FULL_SAFE_PADDING_ROWS = 1;
|
|
26
|
+
export const STARTUP_COMPACT_INTRO_ROWS = 4;
|
|
27
|
+
export const STARTUP_TINY_MESSAGE_ROWS = 3;
|
|
28
|
+
export const transcriptContentIndent = 4; // 2 for DashCard border + 2 for prompt prefix
|
|
29
|
+
const DEFAULT_COLUMNS = 120;
|
|
30
|
+
const DEFAULT_ROWS = 24;
|
|
31
|
+
|
|
32
|
+
export type LayoutMode = "full" | "compact" | "micro";
|
|
33
|
+
export type StartupHeaderMode = "large" | "compact" | "tiny";
|
|
34
|
+
|
|
35
|
+
export interface Layout {
|
|
36
|
+
cols: number;
|
|
37
|
+
rows: number;
|
|
38
|
+
mode: LayoutMode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TerminalViewport extends Layout {
|
|
42
|
+
rawCols?: number;
|
|
43
|
+
rawRows?: number;
|
|
44
|
+
unstable: boolean;
|
|
45
|
+
layoutEpoch: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Dimension helpers ────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function isValidDimension(value: number | undefined): value is number {
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeDimension(value: number | undefined, fallback: number): number {
|
|
55
|
+
if (!isValidDimension(value)) {
|
|
56
|
+
// If even the fallback is invalid, use an absolute floor.
|
|
57
|
+
return isValidDimension(fallback) ? Math.floor(fallback) : 10;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Math.floor(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isRenderableViewport(cols: number | undefined, rows: number | undefined): boolean {
|
|
64
|
+
return isValidDimension(cols)
|
|
65
|
+
&& isValidDimension(rows)
|
|
66
|
+
&& Math.floor(cols) >= MIN_VIEWPORT_COLS
|
|
67
|
+
&& Math.floor(rows) >= MIN_VIEWPORT_ROWS;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Leave a 1-column gutter so box-drawing borders never land exactly on the
|
|
72
|
+
* terminal edge, which can trigger a horizontal scrollbar in some Windows hosts.
|
|
73
|
+
*/
|
|
74
|
+
export function getShellWidth(cols: number | undefined): number {
|
|
75
|
+
return Math.max(20, (cols ?? 120) - 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getUsableShellWidth(cols: number | undefined, reservedColumns = 0): number {
|
|
79
|
+
return Math.max(1, getShellWidth(cols) - reservedColumns);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Leave a 1-row gutter so renders do not land exactly on the terminal bottom
|
|
84
|
+
* edge, which can trigger viewport scroll drift in some hosts.
|
|
85
|
+
*/
|
|
86
|
+
export function getShellHeight(rows: number | undefined): number {
|
|
87
|
+
return Math.max(10, (rows ?? DEFAULT_ROWS) - 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface StartupHeaderModeParams {
|
|
91
|
+
cols: number | undefined;
|
|
92
|
+
rows: number | undefined;
|
|
93
|
+
introRows: number;
|
|
94
|
+
composerRows: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveStartupHeaderMode({
|
|
98
|
+
cols,
|
|
99
|
+
rows,
|
|
100
|
+
introRows,
|
|
101
|
+
composerRows,
|
|
102
|
+
}: StartupHeaderModeParams): StartupHeaderMode {
|
|
103
|
+
const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
|
|
104
|
+
const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
|
|
105
|
+
|
|
106
|
+
if (safeCols < STARTUP_TINY_MIN_COLS || safeRows < STARTUP_TINY_MIN_ROWS) {
|
|
107
|
+
return "tiny";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const shellHeight = getShellHeight(safeRows);
|
|
111
|
+
const fullStartupRows = introRows
|
|
112
|
+
+ composerRows
|
|
113
|
+
+ STARTUP_FULL_MIN_BODY_ROWS
|
|
114
|
+
+ STARTUP_FULL_SAFE_PADDING_ROWS;
|
|
115
|
+
|
|
116
|
+
if (safeCols >= STARTUP_FULL_MIN_COLS && shellHeight >= fullStartupRows) {
|
|
117
|
+
return "large";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "compact";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getVisualWidth(text: string): number {
|
|
124
|
+
return stringWidth(text);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function clampVisualText(text: string, maxWidth: number): string {
|
|
128
|
+
if (maxWidth <= 0) return "";
|
|
129
|
+
if (getVisualWidth(text) <= maxWidth) return text;
|
|
130
|
+
|
|
131
|
+
const ellipsis = maxWidth > 1 ? "…" : "";
|
|
132
|
+
const suffixWidth = getVisualWidth(ellipsis);
|
|
133
|
+
let output = "";
|
|
134
|
+
|
|
135
|
+
for (const char of Array.from(text)) {
|
|
136
|
+
if (getVisualWidth(output + char) + suffixWidth > maxWidth) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
output += char;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return output + ellipsis;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function computeMode(cols: number): LayoutMode {
|
|
146
|
+
if (cols >= BREAKPOINT_FULL) return "full";
|
|
147
|
+
if (cols >= BREAKPOINT_COMPACT) return "compact";
|
|
148
|
+
return "micro";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createLayoutSnapshot(
|
|
152
|
+
cols: number | undefined,
|
|
153
|
+
rows: number | undefined,
|
|
154
|
+
fallback: Layout = {
|
|
155
|
+
cols: DEFAULT_COLUMNS,
|
|
156
|
+
rows: DEFAULT_ROWS,
|
|
157
|
+
mode: computeMode(DEFAULT_COLUMNS),
|
|
158
|
+
},
|
|
159
|
+
): Layout {
|
|
160
|
+
const nextCols = normalizeDimension(cols, fallback.cols);
|
|
161
|
+
const nextRows = normalizeDimension(rows, fallback.rows);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
cols: nextCols,
|
|
165
|
+
rows: nextRows,
|
|
166
|
+
mode: computeMode(nextCols),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function snapshot(stdout: NodeJS.WriteStream, fallback?: Layout): Layout {
|
|
171
|
+
return createLayoutSnapshot(stdout.columns, stdout.rows, fallback);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createTerminalViewport(
|
|
175
|
+
cols: number | undefined,
|
|
176
|
+
rows: number | undefined,
|
|
177
|
+
fallback?: TerminalViewport,
|
|
178
|
+
): TerminalViewport {
|
|
179
|
+
const fallbackLayout = fallback
|
|
180
|
+
? { cols: fallback.cols, rows: fallback.rows, mode: fallback.mode }
|
|
181
|
+
: undefined;
|
|
182
|
+
const unstable = !isRenderableViewport(cols, rows);
|
|
183
|
+
const stableLayout = unstable && fallbackLayout
|
|
184
|
+
? fallbackLayout
|
|
185
|
+
: createLayoutSnapshot(cols, rows, fallbackLayout);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...stableLayout,
|
|
189
|
+
rawCols: cols,
|
|
190
|
+
rawRows: rows,
|
|
191
|
+
unstable,
|
|
192
|
+
layoutEpoch: fallback?.layoutEpoch ?? 0,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function advanceTerminalViewport(
|
|
197
|
+
current: TerminalViewport,
|
|
198
|
+
cols: number | undefined,
|
|
199
|
+
rows: number | undefined,
|
|
200
|
+
): TerminalViewport {
|
|
201
|
+
const next = createTerminalViewport(cols, rows, current);
|
|
202
|
+
if (!next.unstable && current.unstable) {
|
|
203
|
+
return {
|
|
204
|
+
...next,
|
|
205
|
+
layoutEpoch: current.layoutEpoch + 1,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
...next,
|
|
211
|
+
layoutEpoch: current.layoutEpoch,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Hook ────────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/** React hook — returns live layout that ignores transient invalid restore sizes. */
|
|
218
|
+
export function useTerminalViewport(): TerminalViewport {
|
|
219
|
+
const { stdout } = useStdout();
|
|
220
|
+
const [viewport, setViewport] = useState<TerminalViewport>(() => createTerminalViewport(stdout.columns, stdout.rows));
|
|
221
|
+
const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const commit = () => {
|
|
225
|
+
setViewport((current) => {
|
|
226
|
+
const nextViewport = advanceTerminalViewport(current, stdout.columns, stdout.rows);
|
|
227
|
+
if (
|
|
228
|
+
current.cols === nextViewport.cols &&
|
|
229
|
+
current.rows === nextViewport.rows &&
|
|
230
|
+
current.mode === nextViewport.mode &&
|
|
231
|
+
current.unstable === nextViewport.unstable &&
|
|
232
|
+
current.layoutEpoch === nextViewport.layoutEpoch &&
|
|
233
|
+
current.rawCols === nextViewport.rawCols &&
|
|
234
|
+
current.rawRows === nextViewport.rawRows
|
|
235
|
+
) {
|
|
236
|
+
renderDebug.traceFlickerEvent("measurementUpdate", {
|
|
237
|
+
result: "skipped",
|
|
238
|
+
cols: nextViewport.cols,
|
|
239
|
+
rows: nextViewport.rows,
|
|
240
|
+
mode: nextViewport.mode,
|
|
241
|
+
unstable: nextViewport.unstable,
|
|
242
|
+
});
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
renderDebug.traceFlickerEvent("measurementUpdate", {
|
|
247
|
+
result: "updated",
|
|
248
|
+
cols: nextViewport.cols,
|
|
249
|
+
rows: nextViewport.rows,
|
|
250
|
+
mode: nextViewport.mode,
|
|
251
|
+
unstable: nextViewport.unstable,
|
|
252
|
+
});
|
|
253
|
+
return nextViewport;
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const onResize = () => {
|
|
258
|
+
renderDebug.traceEvent("terminal", "viewportHookResize", {
|
|
259
|
+
cols: stdout.columns,
|
|
260
|
+
rows: stdout.rows,
|
|
261
|
+
renderable: isRenderableViewport(stdout.columns, stdout.rows),
|
|
262
|
+
});
|
|
263
|
+
renderDebug.traceLayoutValidity("useTerminalViewport", {
|
|
264
|
+
rawCols: stdout.columns,
|
|
265
|
+
rawRows: stdout.rows,
|
|
266
|
+
});
|
|
267
|
+
commit();
|
|
268
|
+
if (settleTimerRef.current) {
|
|
269
|
+
clearTimeout(settleTimerRef.current);
|
|
270
|
+
}
|
|
271
|
+
settleTimerRef.current = setTimeout(() => {
|
|
272
|
+
settleTimerRef.current = null;
|
|
273
|
+
commit();
|
|
274
|
+
}, RESTORE_SETTLE_MS);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
stdout.on("resize", onResize);
|
|
278
|
+
return () => {
|
|
279
|
+
stdout.off("resize", onResize);
|
|
280
|
+
if (settleTimerRef.current) {
|
|
281
|
+
clearTimeout(settleTimerRef.current);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}, [stdout]);
|
|
285
|
+
|
|
286
|
+
return viewport;
|
|
287
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { getModeDisplaySpec } from "./modeDisplay.js";
|
|
4
|
+
import { THEMES, theme } from "./theme.js";
|
|
5
|
+
|
|
6
|
+
test("maps internal modes to Codex-aligned display labels", () => {
|
|
7
|
+
assert.equal(getModeDisplaySpec("suggest", theme).label, "Read-only");
|
|
8
|
+
assert.equal(getModeDisplaySpec("auto-edit", theme).label, "Auto");
|
|
9
|
+
assert.equal(getModeDisplaySpec("full-auto", theme).label, "Full Access");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("uses distinct ring glyphs and theme tokens for each mode", () => {
|
|
13
|
+
const readOnly = getModeDisplaySpec("suggest", theme);
|
|
14
|
+
const auto = getModeDisplaySpec("auto-edit", theme);
|
|
15
|
+
const fullAccess = getModeDisplaySpec("full-auto", theme);
|
|
16
|
+
|
|
17
|
+
assert.equal(readOnly.ringGlyph, "○");
|
|
18
|
+
assert.equal(readOnly.ringColor, theme.SUCCESS);
|
|
19
|
+
assert.equal(readOnly.ringFill, theme.PANEL_SOFT);
|
|
20
|
+
|
|
21
|
+
assert.equal(auto.ringGlyph, "◎");
|
|
22
|
+
assert.equal(auto.ringColor, theme.BORDER_ACTIVE);
|
|
23
|
+
assert.equal(auto.ringFill, theme.PANEL_ALT);
|
|
24
|
+
|
|
25
|
+
assert.equal(fullAccess.ringGlyph, "◉");
|
|
26
|
+
assert.equal(fullAccess.ringColor, theme.WARNING);
|
|
27
|
+
assert.equal(fullAccess.ringFill, theme.BORDER_SUBTLE);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("keeps the mode ring meaningful across every built-in theme", () => {
|
|
31
|
+
for (const [themeName, activeTheme] of Object.entries(THEMES)) {
|
|
32
|
+
const specs = [
|
|
33
|
+
getModeDisplaySpec("suggest", activeTheme),
|
|
34
|
+
getModeDisplaySpec("auto-edit", activeTheme),
|
|
35
|
+
getModeDisplaySpec("full-auto", activeTheme),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
assert.equal(new Set(specs.map((spec) => spec.ringGlyph)).size, 3, themeName);
|
|
39
|
+
assert.equal(new Set(specs.map((spec) => spec.ringColor)).size, 3, themeName);
|
|
40
|
+
assert.ok(specs.every((spec) => spec.ringFill.length > 0), themeName);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { formatModeLabel } from "../config/settings.js";
|
|
2
|
+
import type { Theme } from "./theme.js";
|
|
3
|
+
|
|
4
|
+
export interface ModeDisplaySpec {
|
|
5
|
+
label: string;
|
|
6
|
+
ringGlyph: string;
|
|
7
|
+
ringColor: string;
|
|
8
|
+
ringFill: string;
|
|
9
|
+
iconColor: string;
|
|
10
|
+
labelColor: string;
|
|
11
|
+
labelBold: boolean;
|
|
12
|
+
ringBold: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getModeDisplaySpec(mode: string, theme: Theme): ModeDisplaySpec {
|
|
16
|
+
switch (mode) {
|
|
17
|
+
case "full-auto":
|
|
18
|
+
return {
|
|
19
|
+
label: formatModeLabel(mode),
|
|
20
|
+
ringGlyph: "◉",
|
|
21
|
+
ringColor: theme.WARNING,
|
|
22
|
+
ringFill: theme.BORDER_SUBTLE,
|
|
23
|
+
iconColor: theme.WARNING,
|
|
24
|
+
labelColor: theme.TEXT,
|
|
25
|
+
labelBold: true,
|
|
26
|
+
ringBold: true,
|
|
27
|
+
};
|
|
28
|
+
case "auto-edit":
|
|
29
|
+
return {
|
|
30
|
+
label: formatModeLabel(mode),
|
|
31
|
+
ringGlyph: "◎",
|
|
32
|
+
ringColor: theme.BORDER_ACTIVE,
|
|
33
|
+
ringFill: theme.PANEL_ALT,
|
|
34
|
+
iconColor: theme.PROMPT,
|
|
35
|
+
labelColor: theme.TEXT,
|
|
36
|
+
labelBold: true,
|
|
37
|
+
ringBold: false,
|
|
38
|
+
};
|
|
39
|
+
case "suggest":
|
|
40
|
+
default:
|
|
41
|
+
return {
|
|
42
|
+
label: formatModeLabel(mode),
|
|
43
|
+
ringGlyph: "○",
|
|
44
|
+
ringColor: theme.SUCCESS,
|
|
45
|
+
ringFill: theme.PANEL_SOFT,
|
|
46
|
+
iconColor: theme.SUCCESS,
|
|
47
|
+
labelColor: theme.MUTED,
|
|
48
|
+
labelBold: false,
|
|
49
|
+
ringBold: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isNoiseLine } from "../core/providers/codexTranscript.js";
|
|
2
|
+
import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
|
|
3
|
+
import { parseMarkdown, type Segment } from "./Markdown.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize: Strip ANSI escape sequences and non-printable control characters.
|
|
7
|
+
* Removes known UI chrome bleed (e.g. box drawing characters) before rendering.
|
|
8
|
+
*
|
|
9
|
+
* Note on diff colours: Diff colouring in this app is applied at the React/Ink
|
|
10
|
+
* rendering layer via getDiffTone() tones, NOT via raw ANSI escape sequences.
|
|
11
|
+
* This means sanitizeTerminalOutput() can safely strip all raw ANSI here —
|
|
12
|
+
* the correct theme colours are re-applied by buildCodePanelRows/buildMarkdownRows
|
|
13
|
+
* in timelineMeasure.ts. There is no need to preserve SGR codes at this stage.
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeOutput(raw: string): string {
|
|
16
|
+
if (!raw) return "";
|
|
17
|
+
const clean = sanitizeTerminalOutput(raw, { preserveTabs: false, tabSize: 2 });
|
|
18
|
+
return clean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize streamed chunks directly (delegates to sanitizeOutput).
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeStreamChunk(chunk: string): string {
|
|
25
|
+
return sanitizeOutput(chunk);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize: Normalizes text formatting for the box wrappers.
|
|
30
|
+
* Replaces CRLF with LF and collapses excessive blank lines to prevent
|
|
31
|
+
* vertical stretching and layout popping.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeOutput(clean: string): string {
|
|
34
|
+
let normalized = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
35
|
+
// Collapse excessive vertical whitespace (4+ newlines into 3)
|
|
36
|
+
normalized = normalized.replace(/\n{4,}/g, "\n\n\n");
|
|
37
|
+
|
|
38
|
+
// Remove lines that are purely known noise prefixes
|
|
39
|
+
const lines = normalized.split("\n");
|
|
40
|
+
const filteredLines = lines.filter((line) => !isNoiseLine(line));
|
|
41
|
+
|
|
42
|
+
return filteredLines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Classify: Segments the normalized string into typed semantic blocks
|
|
47
|
+
* such as prose, code blocks, diffs, lists, and headers.
|
|
48
|
+
*
|
|
49
|
+
* Diff colouring is applied at render time via getDiffTone() in
|
|
50
|
+
* timelineMeasure.ts, which maps each diff line to a TimelineTone that
|
|
51
|
+
* the theme system resolves to the correct terminal colour.
|
|
52
|
+
*/
|
|
53
|
+
export function classifyOutput(normalized: string): Segment[] {
|
|
54
|
+
return parseMarkdown(normalized);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format For Box: Intentional pass-through — segments are returned as-is.
|
|
59
|
+
* Width-fitting is handled downstream by the layout engine in timelineMeasure.ts.
|
|
60
|
+
* This function exists as a named pipeline stage for clarity and future extension.
|
|
61
|
+
*/
|
|
62
|
+
export function formatForBox(classified: Segment[], boxWidth: number): Segment[] {
|
|
63
|
+
return classified;
|
|
64
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { RunProgressBlock, RunProgressEntry, RunProgressSource } from "../session/types.js";
|
|
2
|
+
import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
|
|
3
|
+
import { wrapPlainText } from "./textLayout.js";
|
|
4
|
+
|
|
5
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface VisibleProgressBlock {
|
|
8
|
+
id: string;
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
headline: string;
|
|
12
|
+
text: string;
|
|
13
|
+
source: RunProgressSource;
|
|
14
|
+
entryId: string;
|
|
15
|
+
entrySequence: number;
|
|
16
|
+
blockSequence: number;
|
|
17
|
+
status: RunProgressBlock["status"];
|
|
18
|
+
isLatest: boolean;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VisibleProgressBlocks {
|
|
23
|
+
hiddenCount: number;
|
|
24
|
+
totalCount: number;
|
|
25
|
+
blocks: VisibleProgressBlock[];
|
|
26
|
+
latestBlock: VisibleProgressBlock | null;
|
|
27
|
+
latestActiveBlock: VisibleProgressBlock | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Block selection ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function sourceLabel(source: RunProgressSource): string {
|
|
33
|
+
switch (source) {
|
|
34
|
+
case "reasoning":
|
|
35
|
+
return "Reasoning";
|
|
36
|
+
case "todo":
|
|
37
|
+
return "Todo";
|
|
38
|
+
case "tool":
|
|
39
|
+
return "Tool";
|
|
40
|
+
case "activity":
|
|
41
|
+
return "Activity";
|
|
42
|
+
case "stderr":
|
|
43
|
+
return "Process";
|
|
44
|
+
case "transcript":
|
|
45
|
+
return "Progress";
|
|
46
|
+
default:
|
|
47
|
+
return "Update";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function firstMeaningfulLine(text: string): string {
|
|
52
|
+
return sanitizeTerminalOutput(text)
|
|
53
|
+
.replace(/\r\n/g, "\n")
|
|
54
|
+
.replace(/\r/g, "\n")
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((line) => line.trim())
|
|
57
|
+
.find(Boolean) ?? "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildProgressHeadline(source: RunProgressSource, text: string, status: RunProgressBlock["status"]): string {
|
|
61
|
+
const label = status === "active" ? "Current" : sourceLabel(source);
|
|
62
|
+
const firstLine = firstMeaningfulLine(text);
|
|
63
|
+
return firstLine ? `${label}: ${firstLine}` : label;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toVisibleProgressBlocks(entries: RunProgressEntry[]): VisibleProgressBlock[] {
|
|
67
|
+
const blocks: VisibleProgressBlock[] = [];
|
|
68
|
+
let visibleSequence = 0;
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
for (const block of entry.blocks) {
|
|
72
|
+
if (!block.text.trim()) continue;
|
|
73
|
+
visibleSequence += 1;
|
|
74
|
+
blocks.push({
|
|
75
|
+
id: block.id,
|
|
76
|
+
key: block.id,
|
|
77
|
+
label: `Update ${visibleSequence}`,
|
|
78
|
+
headline: buildProgressHeadline(entry.source, block.text, block.status),
|
|
79
|
+
text: block.text,
|
|
80
|
+
source: entry.source,
|
|
81
|
+
entryId: entry.id,
|
|
82
|
+
entrySequence: entry.sequence,
|
|
83
|
+
blockSequence: block.sequence,
|
|
84
|
+
status: block.status,
|
|
85
|
+
isLatest: false,
|
|
86
|
+
isActive: block.status === "active",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return blocks.map((block, index) => ({
|
|
92
|
+
...block,
|
|
93
|
+
isLatest: index === blocks.length - 1,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getProgressUpdateCount(entries: RunProgressEntry[]): number {
|
|
98
|
+
return toVisibleProgressBlocks(entries).length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function selectVisibleProgressBlocks(entries: RunProgressEntry[], maxVisible: number): VisibleProgressBlocks {
|
|
102
|
+
const blocks = toVisibleProgressBlocks(entries);
|
|
103
|
+
const safeMax = Math.max(0, maxVisible);
|
|
104
|
+
const findLatestActiveBlock = (candidates: VisibleProgressBlock[]): VisibleProgressBlock | null => {
|
|
105
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
106
|
+
if (candidates[index]?.isActive) {
|
|
107
|
+
return candidates[index]!;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (blocks.length <= safeMax) {
|
|
114
|
+
return {
|
|
115
|
+
hiddenCount: 0,
|
|
116
|
+
totalCount: blocks.length,
|
|
117
|
+
blocks,
|
|
118
|
+
latestBlock: blocks[blocks.length - 1] ?? null,
|
|
119
|
+
latestActiveBlock: findLatestActiveBlock(blocks),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const visible = blocks.slice(-safeMax);
|
|
124
|
+
return {
|
|
125
|
+
hiddenCount: blocks.length - safeMax,
|
|
126
|
+
totalCount: blocks.length,
|
|
127
|
+
blocks: visible,
|
|
128
|
+
latestBlock: visible[visible.length - 1] ?? null,
|
|
129
|
+
latestActiveBlock: findLatestActiveBlock(visible),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatProgressBlockBodyLines(text: string, width: number): string[] {
|
|
134
|
+
const contentWidth = Math.max(1, width);
|
|
135
|
+
const normalized = sanitizeTerminalOutput(text)
|
|
136
|
+
.replace(/\r\n/g, "\n")
|
|
137
|
+
.replace(/\r/g, "\n")
|
|
138
|
+
.replace(/[ \t]+\n/g, "\n");
|
|
139
|
+
|
|
140
|
+
if (!normalized.trim()) {
|
|
141
|
+
return [" "];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rows: string[] = [];
|
|
145
|
+
for (const rawLine of normalized.split("\n")) {
|
|
146
|
+
if (!rawLine.trim()) {
|
|
147
|
+
rows.push("");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const wrapped = wrapPlainText(rawLine, contentWidth);
|
|
152
|
+
rows.push(...(wrapped.length > 0 ? wrapped : [" "]));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return rows.length > 0 ? rows : [" "];
|
|
156
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { RunEvent } from "../session/types.js";
|
|
4
|
+
import { summarizeRunActivity } from "../core/workspaceActivity.js";
|
|
5
|
+
import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
|
|
6
|
+
import {
|
|
7
|
+
formatRunActivityStats,
|
|
8
|
+
getVisibleRawOutputLines,
|
|
9
|
+
selectVisibleRunActivity,
|
|
10
|
+
shouldShowRawOutputFallback,
|
|
11
|
+
} from "./runActivityView.js";
|
|
12
|
+
|
|
13
|
+
function makeRunEvent(overrides: Partial<RunEvent> = {}): RunEvent {
|
|
14
|
+
const activity = overrides.activity ?? [];
|
|
15
|
+
return {
|
|
16
|
+
id: 1,
|
|
17
|
+
type: "run",
|
|
18
|
+
createdAt: 1,
|
|
19
|
+
startedAt: 1,
|
|
20
|
+
durationMs: null,
|
|
21
|
+
backendId: "codex-subprocess",
|
|
22
|
+
backendLabel: "Codex CLI",
|
|
23
|
+
runtime: TEST_RUNTIME,
|
|
24
|
+
prompt: "Build something",
|
|
25
|
+
progressEntries: [],
|
|
26
|
+
status: "running",
|
|
27
|
+
summary: "Running",
|
|
28
|
+
truncatedOutput: false,
|
|
29
|
+
toolActivities: overrides.toolActivities ?? [],
|
|
30
|
+
activity,
|
|
31
|
+
touchedFileCount: overrides.touchedFileCount ?? new Set(activity.map((item) => item.path)).size,
|
|
32
|
+
activitySummary: overrides.activitySummary ?? summarizeRunActivity(activity),
|
|
33
|
+
errorMessage: null,
|
|
34
|
+
turnId: overrides.turnId ?? 1,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("does not surface raw output fallback while a run is active", () => {
|
|
40
|
+
const event = makeRunEvent({
|
|
41
|
+
progressEntries: [],
|
|
42
|
+
activity: [{ path: "src/app.tsx", operation: "modified", detectedAt: 1 }],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(shouldShowRawOutputFallback(event), false);
|
|
46
|
+
assert.deepEqual(getVisibleRawOutputLines(event), []);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("keeps raw output hidden even when no structured activity exists yet", () => {
|
|
50
|
+
const event = makeRunEvent({
|
|
51
|
+
progressEntries: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.equal(shouldShowRawOutputFallback(event), false);
|
|
55
|
+
assert.deepEqual(getVisibleRawOutputLines(event), []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("keeps only the compact recent activity for completed runs", () => {
|
|
59
|
+
const activity = Array.from({ length: 6 }, (_, index) => ({
|
|
60
|
+
path: `src/file-${index}.ts`,
|
|
61
|
+
operation: "modified" as const,
|
|
62
|
+
detectedAt: index,
|
|
63
|
+
}));
|
|
64
|
+
const event = makeRunEvent({
|
|
65
|
+
status: "completed",
|
|
66
|
+
activity,
|
|
67
|
+
activitySummary: summarizeRunActivity(activity),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { visible, hiddenCount } = selectVisibleRunActivity(event);
|
|
71
|
+
assert.equal(visible.length, 4);
|
|
72
|
+
assert.equal(hiddenCount, 2);
|
|
73
|
+
assert.equal(visible[0]?.path, "src/file-2.ts");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("formats touched-file stats for the run card header", () => {
|
|
77
|
+
const activity = [
|
|
78
|
+
{ path: "README.md", operation: "created" as const, detectedAt: 1 },
|
|
79
|
+
{ path: "src/app.tsx", operation: "modified" as const, detectedAt: 2 },
|
|
80
|
+
{ path: "old.txt", operation: "deleted" as const, detectedAt: 3 },
|
|
81
|
+
];
|
|
82
|
+
const event = makeRunEvent({
|
|
83
|
+
activity,
|
|
84
|
+
touchedFileCount: 3,
|
|
85
|
+
activitySummary: summarizeRunActivity(activity),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(formatRunActivityStats(event), "3 files touched +1 ~1 -1");
|
|
89
|
+
});
|