@mintlify/cli 4.0.1465 → 4.0.1467

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.
Files changed (40) hide show
  1. package/__test__/mintTestFilePreview.test.ts +35 -0
  2. package/__test__/mintTestOutput.test.ts +194 -0
  3. package/__test__/runHistory.test.ts +164 -0
  4. package/bin/agent-harness/agentPreflight.js +24 -0
  5. package/bin/agent-harness/buildTaskPrompt.js +17 -7
  6. package/bin/agent-harness/countReport.js +10 -0
  7. package/bin/agent-harness/executeCodeBlocks.js +53 -10
  8. package/bin/agent-harness/generateTestCode.js +58 -43
  9. package/bin/agent-harness/index.js +14 -5
  10. package/bin/agent-harness/manifest.js +113 -0
  11. package/bin/agent-harness/runHistory.js +252 -0
  12. package/bin/agent-harness/setupFolders.js +20 -6
  13. package/bin/agent-harness/tasks/checkTestability.js +7 -5
  14. package/bin/agent-harness/types.js +33 -2
  15. package/bin/constants.js +3 -3
  16. package/bin/mintTest.js +31 -9
  17. package/bin/mintTestFilePreview.js +39 -0
  18. package/bin/mintTestText.js +31 -0
  19. package/bin/mintTestUi.js +295 -105
  20. package/bin/status.js +26 -0
  21. package/bin/tsconfig.build.tsbuildinfo +1 -1
  22. package/package.json +7 -7
  23. package/src/agent-harness/MINT_TEST_SYSTEM_DESIGN.md +15 -9
  24. package/src/agent-harness/agentPreflight.ts +13 -0
  25. package/src/agent-harness/buildTaskPrompt.ts +18 -7
  26. package/src/agent-harness/countReport.ts +17 -0
  27. package/src/agent-harness/executeCodeBlocks.ts +67 -1
  28. package/src/agent-harness/generateTestCode.ts +81 -51
  29. package/src/agent-harness/index.ts +20 -5
  30. package/src/agent-harness/manifest.ts +116 -0
  31. package/src/agent-harness/runHistory.ts +250 -0
  32. package/src/agent-harness/setupFolders.ts +19 -5
  33. package/src/agent-harness/tasks/checkTestability.ts +15 -6
  34. package/src/agent-harness/types.ts +81 -5
  35. package/src/constants.ts +6 -2
  36. package/src/mintTest.tsx +34 -8
  37. package/src/mintTestFilePreview.ts +32 -0
  38. package/src/mintTestText.ts +35 -0
  39. package/src/mintTestUi.tsx +586 -224
  40. package/src/status.tsx +24 -0
package/bin/mintTestUi.js CHANGED
@@ -7,27 +7,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
11
  import { Box, render, Text, useInput } from 'ink';
12
12
  import path from 'node:path';
13
13
  import { useEffect, useMemo, useRef, useState } from 'react';
14
14
  import { DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_CONCURRENCY, clearRunSave, runCodeTests, } from './agent-harness/index.js';
15
+ import { readFilePreview } from './mintTestFilePreview.js';
16
+ import { firstLine, footerSegments, taskProblemLabel } from './mintTestText.js';
15
17
  import { toggleScopeSelection, visibleScopeRows } from './mintTestTree.js';
16
18
  const HARNESSES = [
17
19
  { agent: 'claude', model: 'claude-opus-4-8', label: 'Claude Opus 4.8' },
18
20
  { agent: 'claude', model: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
19
21
  { agent: 'codex', model: 'claude-opus-4-8', label: 'Codex' },
20
22
  ];
21
- export function countReport(tasks) {
22
- const counts = { passed: 0, failed: 0, cancelled: 0, agentErrors: 0 };
23
- for (const task of tasks) {
24
- if (task.status === 'agent_error')
25
- counts.agentErrors++;
26
- else
27
- counts[task.status]++;
28
- }
29
- return counts;
30
- }
31
23
  const STEPS = [
32
24
  { id: 'harnesses', label: 'Harnesses' },
33
25
  { id: 'scope', label: 'Scope' },
@@ -74,7 +66,7 @@ function useTerminalSize() {
74
66
  }
75
67
  function stepState(step, screen, activeRunStage) {
76
68
  const order = ['harnesses', 'scope', 'check', 'generate', 'run', 'report'];
77
- const screenStep = screen === 'resume'
69
+ const screenStep = screen === 'resume' || screen === 'existing'
78
70
  ? 'harnesses'
79
71
  : screen === 'running' || screen === 'error'
80
72
  ? activeRunStage
@@ -99,36 +91,38 @@ function StepGlyph({ state }) {
99
91
  return _jsx(Text, { dimColor: true, children: "\u25CB" });
100
92
  }
101
93
  const RESUME_OPTIONS = ['Resume', 'Start fresh'];
94
+ const EXISTING_RUN_OPTIONS = [
95
+ 'Update tests',
96
+ 'Review last test run',
97
+ 'Start a brand new test',
98
+ ];
102
99
  const EMPTY_SELECTION = new Set();
103
100
  function ResumeScreen({ save, cursor }) {
104
101
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Resume previous run?" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Run ", _jsx(Text, { color: "cyan", children: save.runId })] }), _jsxs(Text, { dimColor: true, children: [save.selectedFiles.length, " ", save.selectedFiles.length === 1 ? 'page' : 'pages', " \u00B7", ' ', save.agents.join(', ')] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: RESUME_OPTIONS.map((option, index) => (_jsxs(Text, { color: cursor === index ? 'cyan' : undefined, children: [cursor === index ? '›' : ' ', " ", option] }, option))) })] }));
105
102
  }
103
+ function ExistingRunScreen({ report, cursor }) {
104
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Mint test has already run in this directory." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["Last run ", _jsx(Text, { color: "cyan", children: report.runId }), " \u00B7", ' ', _jsx(Text, { color: report.status === 'passed'
105
+ ? 'green'
106
+ : report.status === 'completed'
107
+ ? 'cyan'
108
+ : report.status === 'cancelled'
109
+ ? 'yellow'
110
+ : 'red', children: report.status })] }), _jsxs(Text, { dimColor: true, children: [report.selectedFiles.length, " ", report.selectedFiles.length === 1 ? 'page' : 'pages', " \u00B7", ' ', report.model, " \u00B7 ", new Date(report.completedAt).toLocaleString()] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EXISTING_RUN_OPTIONS.map((option, index) => (_jsxs(Text, { color: cursor === index ? 'cyan' : undefined, children: [cursor === index ? '›' : ' ', " ", option] }, option))) })] }));
111
+ }
106
112
  function HarnessScreen({ cursor }) {
107
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Which harness should generate and run the tests?" }), _jsx(Text, { dimColor: true, children: "The selected harness tests every page in the chosen scope." }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: HARNESSES.map((harness, index) => (_jsxs(Text, { color: cursor === index ? 'cyan' : undefined, children: [cursor === index ? '›' : ' ', " ", cursor === index ? '(•)' : '( )', " ", harness.label] }, harness.label))) })] }));
113
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select coding harness?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: HARNESSES.map((harness, index) => (_jsxs(Text, { color: cursor === index ? 'cyan' : undefined, children: [cursor === index ? '›' : ' ', " ", cursor === index ? '(•)' : '( )', " ", harness.label] }, harness.label))) })] }));
108
114
  }
109
- function ScopeScreen({ rows, cursor, height, selectedCount, selectedBlocks, testablePages, totalPages, outputDirectory, }) {
115
+ function ScopeScreen({ rows, cursor, height, }) {
110
116
  const viewport = Math.max(4, height - 18);
111
117
  const start = Math.max(0, Math.min(cursor - Math.floor(viewport / 2), rows.length - viewport));
112
118
  const visible = rows.slice(start, start + viewport);
113
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: testablePages }), ' ', "of ", totalPages, " total pages have untested code"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedCount }), ' ', selectedCount === 1 ? 'page' : 'pages', " \u00B7", ' ', _jsx(Text, { color: "cyan", bold: true, children: selectedBlocks }), ' ', selectedBlocks === 1 ? 'code block' : 'code blocks', " will be tested"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Tests generated at ", outputDirectory] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, children: "Choose the docs to test" }) }), _jsx(Text, { dimColor: true, children: "Folders select or clear every testable page beneath them." }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((row, visibleIndex) => {
119
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginTop: 1, marginRight: 2, flexDirection: "column", children: _jsx(Text, { bold: true, children: "Select pages to test" }) }), _jsx(Text, { bold: true, color: "green", children: "enter to confirm" })] }), _jsx(Text, { dimColor: true, children: "Folders select or clear every testable page beneath them." }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((row, visibleIndex) => {
114
120
  const index = start + visibleIndex;
115
121
  const selection = row.selection === 'all' ? '[x]' : row.selection === 'some' ? '[-]' : '[ ]';
116
122
  const disclosure = row.kind === 'directory' ? (row.expanded ? '▾' : '▸') : ' ';
117
123
  return (_jsxs(Text, { color: index === cursor ? 'cyan' : undefined, children: [' '.repeat(row.depth * 2), index === cursor ? '›' : ' ', " ", selection, " ", disclosure, " ", row.label, ' ', _jsxs(Text, { dimColor: true, children: [row.blockCount, " ", row.blockCount === 1 ? 'code block' : 'code blocks'] })] }, row.id));
118
124
  }) })] }));
119
125
  }
120
- const PHASE_LABELS = {
121
- checking: 'validating code can be tested',
122
- checked: 'code can be tested',
123
- not_testable: 'no testable code',
124
- generating: 'generating test code',
125
- generated: 'test generated',
126
- running: 'running tests',
127
- passed: 'passed',
128
- failed: 'tests failed',
129
- agent_error: 'test could not be generated',
130
- cancelled: 'cancelled',
131
- };
132
126
  const ACTIVE_PHASES = new Set(['checking', 'generating', 'running']);
133
127
  const DONE_PHASES = new Set([
134
128
  'passed',
@@ -138,13 +132,120 @@ const DONE_PHASES = new Set([
138
132
  'cancelled',
139
133
  ]);
140
134
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
135
+ const OUTPUT_PHASES = new Set([
136
+ 'generating',
137
+ 'generated',
138
+ 'running',
139
+ 'passed',
140
+ 'failed',
141
+ 'agent_error',
142
+ 'cancelled',
143
+ ]);
144
+ function generatedPathRows(tasks) {
145
+ const rows = [];
146
+ for (const task of tasks) {
147
+ if (!OUTPUT_PHASES.has(task.phase))
148
+ continue;
149
+ if (task.generatedFiles.length === 0) {
150
+ rows.push({
151
+ id: `${task.id}:directory`,
152
+ path: task.directory,
153
+ previewable: false,
154
+ task,
155
+ });
156
+ continue;
157
+ }
158
+ for (const file of task.generatedFiles) {
159
+ rows.push({ id: `${task.id}:${file}`, path: file, previewable: true, task });
160
+ }
161
+ }
162
+ return rows.sort((left, right) => left.path.localeCompare(right.path));
163
+ }
164
+ function useFilePreview(row) {
165
+ const [preview, setPreview] = useState({ status: 'empty' });
166
+ useEffect(() => {
167
+ if (!(row === null || row === void 0 ? void 0 : row.previewable)) {
168
+ setPreview({ status: 'empty' });
169
+ return;
170
+ }
171
+ let active = true;
172
+ setPreview({ status: 'loading' });
173
+ void readFilePreview(row.path)
174
+ .then((result) => {
175
+ if (!active)
176
+ return;
177
+ if (result.binary) {
178
+ setPreview({ status: 'error', message: 'Binary files cannot be previewed.' });
179
+ return;
180
+ }
181
+ setPreview({
182
+ status: 'loaded',
183
+ lines: result.text.split(/\r?\n/),
184
+ truncated: result.truncated,
185
+ });
186
+ })
187
+ .catch((error) => {
188
+ if (!active)
189
+ return;
190
+ setPreview({
191
+ status: 'error',
192
+ message: error instanceof Error ? error.message : String(error),
193
+ });
194
+ });
195
+ return () => {
196
+ active = false;
197
+ };
198
+ }, [row === null || row === void 0 ? void 0 : row.path, row === null || row === void 0 ? void 0 : row.previewable]);
199
+ return preview;
200
+ }
201
+ function generatedPathStatus(phase, spinner) {
202
+ if (phase === 'generating' || phase === 'running')
203
+ return { glyph: spinner, color: 'green' };
204
+ if (phase === 'generated' || phase === 'passed')
205
+ return { glyph: '✓', color: 'green' };
206
+ if (phase === 'failed' || phase === 'agent_error')
207
+ return { glyph: '✗', color: 'red' };
208
+ if (phase === 'cancelled')
209
+ return { glyph: '●', color: 'yellow' };
210
+ return { glyph: '○', dim: true };
211
+ }
212
+ function FilePreview({ row, height }) {
213
+ const preview = useFilePreview(row);
214
+ // Border (2), title, path, margin, and one row kept for the truncation notice.
215
+ const lineLimit = Math.max(1, height - 6);
216
+ return (_jsxs(Box, { width: "52%", height: height, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { bold: true, children: "File preview" }), row ? _jsx(Text, { dimColor: true, children: row.path }) : _jsx(Text, { dimColor: true, children: "Select a generated file." }), !(row === null || row === void 0 ? void 0 : row.previewable) ? (_jsx(Text, { dimColor: true, children: (row === null || row === void 0 ? void 0 : row.task.phase) === 'generating'
217
+ ? 'Files will appear here after generation finishes.'
218
+ : 'No generated file is available to preview.' })) : preview.status === 'loading' ? (_jsx(Text, { dimColor: true, children: "Loading preview..." })) : preview.status === 'error' ? (_jsx(Text, { color: "yellow", children: preview.message })) : preview.status === 'loaded' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [preview.lines.slice(0, lineLimit).map((line, index) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: [String(index + 1).padStart(3), " "] }), line || ' '] }, `${index}:${line}`))), preview.truncated || preview.lines.length > lineLimit ? (_jsx(Text, { dimColor: true, children: "Preview truncated" })) : null] })) : null] }));
219
+ }
220
+ function GeneratedFilesView({ rows, cursor, height, spinner, relativeRoot, emptyMessage = 'Waiting for generated files...', }) {
221
+ const safeCursor = Math.max(0, Math.min(cursor, rows.length - 1));
222
+ const viewport = Math.max(2, height - 3);
223
+ const start = Math.max(0, Math.min(safeCursor - Math.floor(viewport / 2), rows.length - viewport));
224
+ const visible = rows.slice(start, start + viewport);
225
+ const selected = rows[safeCursor];
226
+ const showPreview = (selected === null || selected === void 0 ? void 0 : selected.previewable) === true;
227
+ return (_jsxs(Box, { flexDirection: "row", gap: 1, flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { width: showPreview ? '48%' : '100%', borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { bold: true, children: "Generated files" }), visible.length === 0 ? _jsx(Text, { dimColor: true, children: emptyMessage }) : null, visible.map((row, visibleIndex) => {
228
+ const index = start + visibleIndex;
229
+ const status = generatedPathStatus(row.task.phase, spinner);
230
+ const relativePath = path.relative(relativeRoot, row.path) || '.';
231
+ return (_jsxs(Text, { color: index === safeCursor ? 'cyan' : undefined, wrap: "truncate-end", children: [index === safeCursor ? '›' : ' ', ' ', _jsx(Text, { color: status.color, dimColor: status.dim, children: status.glyph }), ' ', row.previewable
232
+ ? row.path
233
+ : row.task.phase === 'generating'
234
+ ? `Writing under ${relativePath}`
235
+ : `No generated file in ${relativePath}`, row.task.phase === 'failed' || row.task.phase === 'agent_error' ? (_jsxs(Text, { color: "red", children: [' ', taskProblemLabel(row.task.phase), row.task.error ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", firstLine(row.task.error)] }) : null] })) : null] }, row.id));
236
+ })] }), showPreview ? _jsx(FilePreview, { row: selected, height: height }) : null] }));
237
+ }
141
238
  function fileStatus(row, spinner) {
142
239
  if (row && ACTIVE_PHASES.has(row.phase))
143
240
  return { glyph: spinner, color: 'green' };
144
- if ((row === null || row === void 0 ? void 0 : row.phase) === 'not_testable')
145
- return { glyph: '✗', color: 'yellow' };
146
241
  if ((row === null || row === void 0 ? void 0 : row.phase) === 'failed' || (row === null || row === void 0 ? void 0 : row.phase) === 'agent_error')
147
242
  return { glyph: '✗', color: 'red' };
243
+ if ((row === null || row === void 0 ? void 0 : row.phase) === 'checked' || (row === null || row === void 0 ? void 0 : row.phase) === 'generated' || (row === null || row === void 0 ? void 0 : row.phase) === 'passed') {
244
+ return { glyph: '✓', color: 'green' };
245
+ }
246
+ if ((row === null || row === void 0 ? void 0 : row.phase) === 'not_testable' || (row === null || row === void 0 ? void 0 : row.phase) === 'cancelled') {
247
+ return { glyph: '●', color: 'yellow' };
248
+ }
148
249
  return { glyph: '○', dim: true };
149
250
  }
150
251
  const SETTLED_PHASES = new Set([...DONE_PHASES, 'checked', 'generated']);
@@ -153,17 +254,21 @@ function directoryStatus(files, phases, spinner) {
153
254
  if (rows.some((row) => row && ACTIVE_PHASES.has(row.phase))) {
154
255
  return { glyph: spinner, color: 'green' };
155
256
  }
257
+ if (rows.some((row) => (row === null || row === void 0 ? void 0 : row.phase) === 'failed' || (row === null || row === void 0 ? void 0 : row.phase) === 'agent_error')) {
258
+ return { glyph: '✗', color: 'red' };
259
+ }
156
260
  if (rows.every((row) => row && SETTLED_PHASES.has(row.phase))) {
157
- if (rows.some((row) => row && ['failed', 'agent_error'].includes(row.phase))) {
158
- return { glyph: '●', color: 'red' };
261
+ if (rows.every((row) => (row === null || row === void 0 ? void 0 : row.phase) === 'not_testable' || (row === null || row === void 0 ? void 0 : row.phase) === 'cancelled')) {
262
+ return { glyph: '●', color: 'yellow' };
159
263
  }
160
- return rows.every((row) => row && DONE_PHASES.has(row.phase))
161
- ? { glyph: '●', color: 'green' }
162
- : { glyph: '●', dim: true };
264
+ return { glyph: '✓', color: 'green' };
163
265
  }
164
266
  return { glyph: '○', dim: true };
165
267
  }
166
- function ActivityScreen({ stage, cancelling, harness, rows, cursor, phases, height, totalTasks, spinnerFrame, runStartedAt, now, }) {
268
+ function ActivityLegend({ spinner }) {
269
+ return (_jsxs(Text, { dimColor: true, children: ["\u25CB waiting \u00B7 ", _jsx(Text, { color: "green", children: spinner }), " active \u00B7 ", _jsx(Text, { color: "green", children: "\u2713" }), ' ', "complete \u00B7 ", _jsx(Text, { color: "red", children: "\u2717" }), " failed \u00B7 ", _jsx(Text, { color: "yellow", children: "\u25CF" }), " skipped / cancelled"] }));
270
+ }
271
+ function ActivityScreen({ stage, cancelling, harness, docsRoot, rows, generatedRows, cursor, phases, height, totalTasks, spinnerFrame, runStartedAt, now, }) {
167
272
  var _a;
168
273
  const spinner = (_a = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]) !== null && _a !== void 0 ? _a : '⠋';
169
274
  const updates = [...phases.values()];
@@ -187,46 +292,48 @@ function ActivityScreen({ stage, cancelling, harness, rows, cursor, phases, heig
187
292
  const viewport = Math.max(4, height - 13);
188
293
  const start = Math.max(0, Math.min(cursor - Math.floor(viewport / 2), rows.length - viewport));
189
294
  const visible = rows.slice(start, start + viewport);
190
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [padDuration(now - runStartedAt), " ", _jsxs(Text, { dimColor: true, children: [padTokens(tokens), " tokens"] })] }), _jsxs(Text, { color: "green", children: [_jsx(Text, { children: spinner }), " ", message] }), _jsxs(Text, { dimColor: true, children: ["Harness: ", harness] }), _jsx(Text, { dimColor: true, children: progress })] }), _jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, flexDirection: "column", flexGrow: 1, children: visible.map((row, visibleIndex) => {
191
- var _a;
192
- const index = start + visibleIndex;
193
- const disclosure = row.kind === 'directory' ? (row.expanded ? '▾' : '▸') : ' ';
194
- const status = row.kind === 'directory'
195
- ? directoryStatus(row.selectableFiles, phases, spinner)
196
- : fileStatus(row.file ? phases.get(row.file) : undefined, spinner);
197
- const task = row.file ? phases.get(row.file) : undefined;
198
- const settled = row.kind === 'directory'
199
- ? row.selectableFiles.filter((file) => {
200
- var _a;
201
- const phase = (_a = phases.get(file)) === null || _a === void 0 ? void 0 : _a.phase;
202
- return phase !== undefined && SETTLED_PHASES.has(phase);
203
- }).length
204
- : 0;
205
- return (_jsxs(Text, { color: index === cursor ? 'cyan' : undefined, wrap: "truncate-end", children: [' '.repeat(row.depth * 2), index === cursor ? '›' : ' ', ' ', _jsx(Text, { color: status.color, dimColor: status.dim, children: status.glyph }), ' ', disclosure, " ", row.label, row.kind === 'directory' ? (_jsxs(Text, { dimColor: true, children: [' ', padCount(settled, row.selectableFiles.length), "/", row.selectableFiles.length, ' ', "complete"] })) : null, task ? (_jsxs(Text, { dimColor: true, children: [" ", padDuration(((_a = task.finishedAt) !== null && _a !== void 0 ? _a : now) - task.startedAt)] })) : null] }, row.id));
206
- }) })] }));
295
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [padDuration(now - runStartedAt), " ", _jsxs(Text, { dimColor: true, children: [padTokens(tokens), " tokens"] })] }), _jsxs(Text, { color: "green", children: [_jsx(Text, { children: spinner }), " ", message] }), _jsxs(Text, { dimColor: true, children: ["Harness: ", harness] }), _jsx(Text, { dimColor: true, children: progress }), stage === 'check' || stage === 'generate' ? _jsx(ActivityLegend, { spinner: spinner }) : null] }), _jsx(Box, { marginTop: 1, flexGrow: 1, overflow: "hidden", children: stage === 'check' ? (_jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", flexGrow: 1, children: visible.map((row, visibleIndex) => {
296
+ var _a;
297
+ const index = start + visibleIndex;
298
+ const disclosure = row.kind === 'directory' ? (row.expanded ? '▾' : '▸') : ' ';
299
+ const status = row.kind === 'directory'
300
+ ? directoryStatus(row.selectableFiles, phases, spinner)
301
+ : fileStatus(row.file ? phases.get(row.file) : undefined, spinner);
302
+ const task = row.file ? phases.get(row.file) : undefined;
303
+ const settled = row.kind === 'directory'
304
+ ? row.selectableFiles.filter((file) => {
305
+ var _a;
306
+ const phase = (_a = phases.get(file)) === null || _a === void 0 ? void 0 : _a.phase;
307
+ return phase !== undefined && SETTLED_PHASES.has(phase);
308
+ }).length
309
+ : 0;
310
+ return (_jsxs(Text, { color: index === cursor ? 'cyan' : undefined, wrap: "truncate-end", children: [' '.repeat(row.depth * 2), index === cursor ? '›' : ' ', ' ', _jsx(Text, { color: status.color, dimColor: status.dim, children: status.glyph }), ' ', disclosure, " ", row.label, row.kind === 'directory' ? (_jsxs(Text, { dimColor: true, children: [' ', padCount(settled, row.selectableFiles.length), "/", row.selectableFiles.length, ' ', "complete"] })) : null, task ? (_jsxs(Text, { dimColor: true, children: [" ", padDuration(((_a = task.finishedAt) !== null && _a !== void 0 ? _a : now) - task.startedAt)] })) : null, task && (task.phase === 'failed' || task.phase === 'agent_error') ? (_jsxs(Text, { color: "red", children: [' ', taskProblemLabel(task.phase), task.error ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", firstLine(task.error)] }) : null] })) : null] }, row.id));
311
+ }) })) : (_jsx(GeneratedFilesView, { rows: generatedRows, cursor: cursor, height: Math.max(4, height - 7), spinner: spinner, relativeRoot: docsRoot })) })] }));
207
312
  }
208
- function reportFileStatus(row) {
209
- if ((row === null || row === void 0 ? void 0 : row.phase) === 'passed')
210
- return { glyph: '', color: 'green' };
211
- if ((row === null || row === void 0 ? void 0 : row.phase) === 'failed' || (row === null || row === void 0 ? void 0 : row.phase) === 'agent_error')
212
- return { glyph: '✗', color: 'red' };
213
- if ((row === null || row === void 0 ? void 0 : row.phase) === 'not_testable')
214
- return { glyph: '', color: 'yellow' };
215
- if ((row === null || row === void 0 ? void 0 : row.phase) === 'cancelled')
216
- return { glyph: '●', color: 'yellow' };
217
- return { glyph: '○', dim: true };
313
+ const MAX_PROBLEM_ROWS = 6;
314
+ function ProblemsView({ report }) {
315
+ const problems = report.tasks.filter((task) => task.status === 'failed' || task.status === 'agent_error');
316
+ if (problems.length === 0)
317
+ return null;
318
+ const hidden = problems.length - MAX_PROBLEM_ROWS;
319
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { bold: true, color: "red", children: [problems.length, " ", problems.length === 1 ? 'page needs' : 'pages need', " attention"] }), problems.slice(0, MAX_PROBLEM_ROWS).map((task) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "red", children: "\u2717" }), " ", task.file, ' ', _jsx(Text, { dimColor: true, children: taskProblemLabel(task.status) }), task.error ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", firstLine(task.error)] }) : null] }, task.id))), hidden > 0 ? _jsxs(Text, { dimColor: true, children: ["+", hidden, " more in the report"] }) : null] }));
218
320
  }
219
- function reportDirectoryStatus(files, phases) {
220
- const rows = files.map((file) => phases.get(file));
221
- if (rows.some((row) => (row === null || row === void 0 ? void 0 : row.phase) === 'failed' || (row === null || row === void 0 ? void 0 : row.phase) === 'agent_error')) {
222
- return { glyph: '✗', color: 'red' };
223
- }
224
- if (rows.length > 0 && rows.every((row) => (row === null || row === void 0 ? void 0 : row.phase) === 'passed')) {
225
- return { glyph: '✓', color: 'green' };
226
- }
227
- return { glyph: '●', color: 'yellow' };
321
+ /**
322
+ * Rows the report header takes, including its border and the margin below it. The optional
323
+ * lines must be counted, or the screen is one row taller than the terminal and Ink drops the
324
+ * heading (it showed as "Finished in 1m 37s passed").
325
+ */
326
+ function reportHeaderHeight(report, rows) {
327
+ const optionalLines = (report.historyError ? 1 : 0) + (rows.some((row) => row.previewable) ? 1 : 0);
328
+ return 2 + 4 + optionalLines + 1;
228
329
  }
229
- function ReportScreen({ report, rows, cursor, phases, height, }) {
330
+ function problemsViewHeight(report) {
331
+ const problems = report.tasks.filter((task) => task.status === 'failed' || task.status === 'agent_error').length;
332
+ if (problems === 0)
333
+ return 0;
334
+ return Math.min(problems, MAX_PROBLEM_ROWS) + 4 + (problems > MAX_PROBLEM_ROWS ? 1 : 0);
335
+ }
336
+ function ReportScreen({ report, rows, cursor, height, }) {
230
337
  const statusColor = report.status === 'passed'
231
338
  ? 'green'
232
339
  : report.status === 'completed'
@@ -239,46 +346,43 @@ function ReportScreen({ report, rows, cursor, phases, height, }) {
239
346
  : report.status === 'completed'
240
347
  ? 'Run completed'
241
348
  : `Run ${report.status}`;
242
- const viewport = Math.max(4, height - 12);
243
- const start = Math.max(0, Math.min(cursor - Math.floor(viewport / 2), rows.length - viewport));
244
- const visible = rows.slice(start, start + viewport);
245
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: statusColor, children: heading }), _jsxs(Text, { children: ["Finished in ", _jsx(Text, { bold: true, children: formatDuration(report.durationMs) })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Report at ", report.reportPath] })] }), _jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, flexDirection: "column", flexGrow: 1, children: visible.map((row, visibleIndex) => {
246
- const index = start + visibleIndex;
247
- const disclosure = row.kind === 'directory' ? (row.expanded ? '▾' : '▸') : ' ';
248
- const status = row.kind === 'directory'
249
- ? reportDirectoryStatus(row.selectableFiles, phases)
250
- : reportFileStatus(row.file ? phases.get(row.file) : undefined);
251
- const task = row.file ? phases.get(row.file) : undefined;
252
- const passed = row.kind === 'directory'
253
- ? row.selectableFiles.filter((file) => { var _a; return ((_a = phases.get(file)) === null || _a === void 0 ? void 0 : _a.phase) === 'passed'; }).length
254
- : 0;
255
- return (_jsxs(Text, { color: index === cursor ? 'cyan' : undefined, wrap: "truncate-end", children: [' '.repeat(row.depth * 2), index === cursor ? '›' : ' ', ' ', _jsx(Text, { color: status.color, dimColor: status.dim, children: status.glyph }), ' ', disclosure, " ", row.label, row.kind === 'directory' ? (_jsxs(Text, { dimColor: true, children: [' ', padCount(passed, row.selectableFiles.length), "/", row.selectableFiles.length, " passed"] })) : task ? (_jsxs(Text, { dimColor: true, children: [' ', PHASE_LABELS[task.phase], task.finishedAt ? ` · ${formatDuration(task.finishedAt - task.startedAt)}` : ''] })) : null] }, row.id));
256
- }) })] }));
349
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { bold: true, color: statusColor, children: heading }), _jsxs(Text, { children: ["Finished in ", _jsx(Text, { bold: true, children: formatDuration(report.durationMs) })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Report at ", report.reportPath] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["Generated files at ", report.outputDirectory] }), report.historyError ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: ["Warning: ", report.historyError] })) : null, rows.some((row) => row.previewable) ? (_jsx(Text, { children: "Select a file below to preview its contents." })) : null] }), _jsx(ProblemsView, { report: report }), _jsx(Box, { marginTop: 1, flexGrow: 1, overflow: "hidden", children: _jsx(GeneratedFilesView, { rows: rows, cursor: cursor, height: Math.max(4, height - reportHeaderHeight(report, rows) - problemsViewHeight(report)), spinner: "\u25CF", relativeRoot: report.docsRoot, emptyMessage: "No generated files." }) })] }));
257
350
  }
258
- function footerForScreen(screen, running, cancelled) {
259
- if (running)
260
- return '↑/↓/j/k move · ←/→/h/l collapse/expand · Ctrl-C cancel';
351
+ function FooterHints({ text }) {
352
+ return (_jsx(_Fragment, { children: footerSegments(text).map((segment, index) => (_jsxs(Text, { children: [index > 0 ? ' · ' : '', _jsx(Text, { bold: true, children: segment.key }), segment.action ? ` ${segment.action}` : ''] }, `${index}:${segment.key}`))) }));
353
+ }
354
+ function footerForScreen(screen, running, cancelled, activeRunStage, reviewingSavedRun) {
355
+ if (running) {
356
+ return activeRunStage === 'check'
357
+ ? '↑/↓/j/k move · ←/→/h/l collapse/expand · Ctrl-C cancel'
358
+ : '↑/↓/j/k preview generated files · Ctrl-C cancel';
359
+ }
261
360
  if (screen === 'resume')
262
361
  return '↑/↓/j/k move · Enter confirm · Ctrl-C cancel';
362
+ if (screen === 'existing')
363
+ return '↑/↓/j/k move · Enter confirm · Ctrl-C cancel';
263
364
  if (screen === 'harnesses')
264
365
  return '↑/↓/j/k move · Enter continue · Ctrl-C cancel';
265
366
  if (screen === 'scope') {
266
367
  return '↑/↓/j/k move · ←/→/h/l collapse/expand · Space select · a all · Enter start';
267
368
  }
268
369
  if (screen === 'report') {
370
+ if (reviewingSavedRun)
371
+ return '↑/↓/j/k preview generated files · Enter exit';
269
372
  return cancelled
270
- ? '↑/↓/j/k move · ←/→/h/l collapse/expand · Ctrl-C exit · save kept for resume'
271
- : '↑/↓/j/k move · ←/→/h/l collapse/expand · Enter exit';
373
+ ? '↑/↓/j/k preview generated files · Ctrl-C exit · save kept for resume'
374
+ : '↑/↓/j/k preview generated files · Enter exit';
272
375
  }
273
376
  return 'Enter exit';
274
377
  }
275
- function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPages, save, onFinish, }) {
378
+ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, save, lastReport, onFinish, }) {
276
379
  var _a;
277
380
  const size = useTerminalSize();
278
381
  const files = useMemo(() => pages.map((page) => page.file), [pages]);
279
382
  const outputDirectory = path.join(docsRoot, 'tests', 'mint-test');
280
- const [screen, setScreen] = useState(save ? 'resume' : 'harnesses');
383
+ const [screen, setScreen] = useState(save ? 'resume' : lastReport ? 'existing' : 'harnesses');
281
384
  const [resumeCursor, setResumeCursor] = useState(0);
385
+ const [existingRunCursor, setExistingRunCursor] = useState(0);
282
386
  const [harnessIndex, setHarnessIndex] = useState(0);
283
387
  const [runHarness, setRunHarness] = useState('');
284
388
  const harness = (_a = HARNESSES[harnessIndex]) !== null && _a !== void 0 ? _a : HARNESSES[0];
@@ -297,9 +401,11 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
297
401
  const [runStartedAt, setRunStartedAt] = useState(0);
298
402
  const autoCollapsed = useRef(new Set());
299
403
  const manualCursor = useRef(false);
404
+ const previousRunStage = useRef(activeRunStage);
300
405
  const [spinnerFrame, setSpinnerFrame] = useState(0);
301
406
  const [now, setNow] = useState(() => Date.now());
302
407
  const [report, setReport] = useState();
408
+ const [reviewingSavedRun, setReviewingSavedRun] = useState(false);
303
409
  const [error, setError] = useState();
304
410
  const abortController = useRef(undefined);
305
411
  const finished = useRef(false);
@@ -310,6 +416,7 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
310
416
  const included = new Set(visibleRunFiles);
311
417
  return visibleScopeRows(pages.filter((page) => included.has(page.file)), runExpanded, EMPTY_SELECTION);
312
418
  }, [pages, visibleRunFiles, runExpanded]);
419
+ const generatedRows = useMemo(() => generatedPathRows(taskPhases.values()), [taskPhases]);
313
420
  useEffect(() => {
314
421
  if (screen !== 'running')
315
422
  return;
@@ -322,7 +429,7 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
322
429
  }, [screen]);
323
430
  useEffect(() => {
324
431
  var _a;
325
- if (screen !== 'running')
432
+ if (screen !== 'running' || activeRunStage !== 'check')
326
433
  return;
327
434
  const fileDone = (file) => {
328
435
  var _a;
@@ -364,19 +471,56 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
364
471
  if (nextIndex !== -1 && nextIndex !== runCursor)
365
472
  setRunCursor(nextIndex);
366
473
  }
367
- }, [screen, phasesByFile, visibleRunFiles, runRows, runCursor]);
474
+ }, [screen, activeRunStage, phasesByFile, visibleRunFiles, runRows, runCursor]);
475
+ useEffect(() => {
476
+ if (previousRunStage.current === activeRunStage)
477
+ return;
478
+ previousRunStage.current = activeRunStage;
479
+ manualCursor.current = false;
480
+ setRunCursor(0);
481
+ }, [activeRunStage]);
368
482
  const finish = (exitCode) => {
369
483
  if (finished.current)
370
484
  return;
371
485
  finished.current = true;
372
486
  onFinish(exitCode);
373
487
  };
488
+ const reviewRun = (savedReport) => {
489
+ var _a, _b;
490
+ const startedAt = Date.parse(savedReport.startedAt);
491
+ const finishedAt = Date.parse(savedReport.completedAt);
492
+ setTaskPhases(new Map(savedReport.tasks.map((task) => [
493
+ task.id,
494
+ {
495
+ id: task.id,
496
+ agent: task.agent,
497
+ file: task.file,
498
+ directory: task.directory,
499
+ generatedFiles: task.generatedFiles,
500
+ phase: !task.testable && task.status === 'passed' ? 'not_testable' : task.status,
501
+ tokens: task.tokens,
502
+ startedAt: Number.isNaN(startedAt) ? 0 : startedAt,
503
+ finishedAt: Number.isNaN(finishedAt) ? undefined : finishedAt,
504
+ },
505
+ ])));
506
+ setRunFiles(savedReport.selectedFiles);
507
+ setRunHarness((_b = (_a = HARNESSES.find((entry) => entry.agent === savedReport.agents[0] && entry.model === savedReport.model)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : `${savedReport.agents.join(', ')} · ${savedReport.model}`);
508
+ setReport(savedReport);
509
+ setReviewingSavedRun(true);
510
+ setActiveRunStage('run');
511
+ setRunCursor(0);
512
+ setScreen('report');
513
+ };
374
514
  const startRun = ({ runId, runAgents, runModel, runFiles: runFiles_, }) => {
375
515
  var _a, _b;
376
516
  if (abortController.current)
377
517
  return;
378
518
  const controller = new AbortController();
379
519
  abortController.current = controller;
520
+ setTaskPhases(new Map());
521
+ setReport(undefined);
522
+ setReviewingSavedRun(false);
523
+ setError(undefined);
380
524
  setTotalTasks(runAgents.length * runFiles_.length);
381
525
  setRunHarness((_b = (_a = HARNESSES.find((entry) => entry.agent === runAgents[0] && entry.model === runModel)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : `${runAgents.join(', ')} · ${runModel}`);
382
526
  setRunCursor(0);
@@ -462,15 +606,17 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
462
606
  if (screen === 'running' || screen === 'report') {
463
607
  const collapse = key.leftArrow || input === 'h';
464
608
  const expand = key.rightArrow || input === 'l';
609
+ const showingGeneratedFiles = screen === 'report' || activeRunStage !== 'check';
610
+ const rowCount = showingGeneratedFiles ? generatedRows.length : runRows.length;
465
611
  if (key.upArrow || input === 'k') {
466
612
  manualCursor.current = true;
467
613
  setRunCursor((current) => Math.max(0, current - 1));
468
614
  }
469
615
  else if (key.downArrow || input === 'j') {
470
616
  manualCursor.current = true;
471
- setRunCursor((current) => Math.max(0, Math.min(runRows.length - 1, current + 1)));
617
+ setRunCursor((current) => Math.max(0, Math.min(rowCount - 1, current + 1)));
472
618
  }
473
- else if (collapse || expand) {
619
+ else if ((collapse || expand) && !showingGeneratedFiles) {
474
620
  manualCursor.current = true;
475
621
  const row = runRows[runCursor];
476
622
  if ((row === null || row === void 0 ? void 0 : row.kind) !== 'directory')
@@ -485,7 +631,50 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
485
631
  });
486
632
  }
487
633
  else if (screen === 'report' && key.return && report) {
488
- finish(report.status === 'passed' ? 0 : report.status === 'cancelled' ? 130 : 1);
634
+ finish(reviewingSavedRun
635
+ ? 0
636
+ : report.status === 'passed'
637
+ ? 0
638
+ : report.status === 'cancelled'
639
+ ? 130
640
+ : 1);
641
+ }
642
+ return;
643
+ }
644
+ if (screen === 'existing' && lastReport) {
645
+ if (key.upArrow || input === 'k') {
646
+ setExistingRunCursor((current) => (current + EXISTING_RUN_OPTIONS.length - 1) % EXISTING_RUN_OPTIONS.length);
647
+ }
648
+ else if (key.downArrow || input === 'j') {
649
+ setExistingRunCursor((current) => (current + 1) % EXISTING_RUN_OPTIONS.length);
650
+ }
651
+ else if (key.return) {
652
+ if (existingRunCursor === 0) {
653
+ const knownFiles = new Set(files);
654
+ const updateFiles = lastReport.selectedFiles.filter((file) => knownFiles.has(file));
655
+ setSelectedFiles(new Set(updateFiles.length > 0 ? updateFiles : autoSelectedFiles));
656
+ if (updateFiles.length === 0) {
657
+ const previousHarness = HARNESSES.findIndex((entry) => entry.agent === lastReport.agents[0] && entry.model === lastReport.model);
658
+ if (previousHarness !== -1)
659
+ setHarnessIndex(previousHarness);
660
+ setScreen('scope');
661
+ }
662
+ else {
663
+ startRun({
664
+ runId: null,
665
+ runAgents: lastReport.agents,
666
+ runModel: lastReport.model,
667
+ runFiles: updateFiles,
668
+ });
669
+ }
670
+ }
671
+ else if (existingRunCursor === 1) {
672
+ reviewRun(lastReport);
673
+ }
674
+ else {
675
+ setSelectedFiles(new Set(autoSelectedFiles));
676
+ setScreen('harnesses');
677
+ }
489
678
  }
490
679
  return;
491
680
  }
@@ -568,27 +757,28 @@ function MintTestApp({ targetPath, docsRoot, pages, autoSelectedFiles, totalPage
568
757
  let content;
569
758
  if (screen === 'resume' && save)
570
759
  content = _jsx(ResumeScreen, { save: save, cursor: resumeCursor });
760
+ else if (screen === 'existing' && lastReport) {
761
+ content = _jsx(ExistingRunScreen, { report: lastReport, cursor: existingRunCursor });
762
+ }
571
763
  else if (screen === 'harnesses') {
572
764
  content = _jsx(HarnessScreen, { cursor: harnessIndex });
573
765
  }
574
766
  else if (screen === 'scope') {
575
- content = (_jsx(ScopeScreen, { rows: rows, cursor: scopeCursor, height: viewHeight, selectedCount: selectedFiles.size, selectedBlocks: pages.reduce((total, page) => total + (selectedFiles.has(page.file) ? page.blockCount : 0), 0), testablePages: pages.length, totalPages: totalPages, outputDirectory: outputDirectory }));
767
+ content = _jsx(ScopeScreen, { rows: rows, cursor: scopeCursor, height: viewHeight });
576
768
  }
577
769
  else if (screen === 'report' && report) {
578
- content = (_jsx(ReportScreen, { report: report, rows: runRows, cursor: runCursor, phases: phasesByFile, height: viewHeight }));
770
+ content = (_jsx(ReportScreen, { report: report, rows: generatedRows, cursor: runCursor, height: viewHeight }));
579
771
  }
580
772
  else if (screen === 'error') {
581
773
  content = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: "Mint test stopped" }), _jsx(Text, { children: error !== null && error !== void 0 ? error : 'Unknown error' })] }));
582
774
  }
583
775
  else {
584
- content = (_jsx(ActivityScreen, { stage: activeRunStage, cancelling: cancelling, harness: runHarness, rows: runRows, cursor: runCursor, phases: phasesByFile, height: viewHeight, totalTasks: totalTasks, spinnerFrame: spinnerFrame, runStartedAt: runStartedAt, now: now }));
776
+ content = (_jsx(ActivityScreen, { stage: activeRunStage, cancelling: cancelling, harness: runHarness, docsRoot: docsRoot, rows: runRows, generatedRows: generatedRows, cursor: runCursor, phases: phasesByFile, height: viewHeight, totalTasks: totalTasks, spinnerFrame: spinnerFrame, runStartedAt: runStartedAt, now: now }));
585
777
  }
586
778
  return (_jsxs(Box, { flexDirection: "column", width: size.columns, height: viewHeight, overflow: "hidden", children: [_jsxs(Box, { flexGrow: 1, gap: 1, overflow: "hidden", children: [_jsxs(Box, { width: leftWidth, flexShrink: 0, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Steps" }), STEPS.map((step) => {
587
779
  const state = stepState(step.id, screen, activeRunStage);
588
780
  return (_jsxs(Text, { color: state === 'running' ? 'cyan' : undefined, children: [_jsx(StepGlyph, { state: state }), " ", step.label] }, step.id));
589
- })] }), _jsx(Box, { flexGrow: 1, borderStyle: fullBleed ? undefined : 'round', borderColor: "gray", paddingX: fullBleed ? 0 : 1, flexDirection: "column", overflow: "hidden", children: content })] }), _jsx(Text, { dimColor: true, children: ctrlCArmed
590
- ? 'Ctrl-C again to exit'
591
- : footerForScreen(screen, running, (report === null || report === void 0 ? void 0 : report.status) === 'cancelled') })] }));
781
+ })] }), _jsx(Box, { flexGrow: 1, borderStyle: fullBleed ? undefined : 'round', borderColor: "gray", paddingX: fullBleed ? 0 : 1, flexDirection: "column", overflow: "hidden", children: content })] }), _jsx(Text, { dimColor: true, children: ctrlCArmed ? (_jsx(FooterHints, { text: "Ctrl-C again to exit" })) : (_jsx(FooterHints, { text: footerForScreen(screen, running, (report === null || report === void 0 ? void 0 : report.status) === 'cancelled', activeRunStage, reviewingSavedRun) })) })] }));
592
782
  }
593
783
  export function runMintTestUi(options) {
594
784
  return __awaiter(this, void 0, void 0, function* () {