@f5xc-salesdemos/xcsh 18.53.1 → 18.55.0
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/CHANGELOG.md +5 -0
- package/package.json +7 -7
- package/src/autoresearch/apply-contract-to-state.ts +1 -24
- package/src/autoresearch/contract.ts +77 -130
- package/src/autoresearch/dashboard.ts +31 -70
- package/src/autoresearch/git.ts +22 -124
- package/src/autoresearch/helpers.ts +120 -236
- package/src/autoresearch/index.ts +50 -128
- package/src/autoresearch/state.ts +96 -123
- package/src/autoresearch/types.ts +23 -98
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/session/messages.ts +57 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## [18.53.0] - 2026-05-09
|
|
6
6
|
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Autoresearch subsystem code quality: -513 lines (18.8% reduction), ~13% faster type checking. Un-exported internal symbols, relocated types, consolidated duplicate patterns, replaced manual deep copies with `structuredClone`, replaced `while(exec)` with `matchAll`, compressed control flow, extracted shared interfaces ([#734](https://github.com/f5xc-salesdemos/xcsh/pull/734))
|
|
10
|
+
## [18.53.0] - 2026-05-09
|
|
11
|
+
|
|
7
12
|
### Fixed
|
|
8
13
|
|
|
9
14
|
- Replaced `xcsh --version` recommendation in `renderAboutDoc()` with authoritative intrinsic version guidance — the previous guidance misdirected to the installed binary, not the running session ([#722](https://github.com/f5xc-salesdemos/xcsh/pull/722))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.55.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
50
50
|
"@mozilla/readability": "^0.6",
|
|
51
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.55.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.55.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.55.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.55.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.55.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.55.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { AutoresearchContract, ExperimentState } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Updates session fields from a validated `autoresearch.md` parse (same fields as `init_experiment`).
|
|
6
|
-
* Does not touch `name`, `currentSegment`, `results`, `bestMetric`, `confidence`, or `maxExperiments`.
|
|
7
|
-
*/
|
|
8
|
-
export function applyAutoresearchContractToExperimentState(
|
|
9
|
-
contract: AutoresearchContract,
|
|
10
|
-
state: ExperimentState,
|
|
11
|
-
): void {
|
|
12
|
-
const benchmarkContract = contract.benchmark;
|
|
13
|
-
state.metricName = benchmarkContract.primaryMetric ?? state.metricName;
|
|
14
|
-
state.metricUnit = benchmarkContract.metricUnit;
|
|
15
|
-
state.bestDirection = benchmarkContract.direction ?? "lower";
|
|
16
|
-
state.secondaryMetrics = benchmarkContract.secondaryMetrics.map(name => ({
|
|
17
|
-
name,
|
|
18
|
-
unit: inferMetricUnitFromName(name),
|
|
19
|
-
}));
|
|
20
|
-
state.benchmarkCommand = benchmarkContract.command?.trim() ?? state.benchmarkCommand;
|
|
21
|
-
state.scopePaths = [...contract.scopePaths];
|
|
22
|
-
state.offLimits = [...contract.offLimits];
|
|
23
|
-
state.constraints = [...contract.constraints];
|
|
24
|
-
}
|
|
1
|
+
export { applyAutoresearchContractToExperimentState } from "./contract";
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
export interface AutoresearchContractLoadResult {
|
|
6
|
-
contract: AutoresearchContract;
|
|
7
|
-
errors: string[];
|
|
8
|
-
path: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface AutoresearchScriptSnapshot {
|
|
12
|
-
benchmarkScript: string;
|
|
13
|
-
benchmarkScriptPath: string;
|
|
14
|
-
checksScript: string | null;
|
|
15
|
-
checksScriptPath: string;
|
|
16
|
-
errors: string[];
|
|
17
|
-
}
|
|
3
|
+
import { inferMetricUnitFromName } from "./helpers";
|
|
4
|
+
import type { AutoresearchContract, ExperimentState, MetricDirection } from "./types";
|
|
18
5
|
|
|
19
6
|
const HEADING_REGEX = /^##\s+(.+?)\s*$/;
|
|
20
7
|
const LIST_ITEM_REGEX = /^\s*[-*]\s+(.*)$/;
|
|
21
8
|
const KEY_VALUE_REGEX = /^\s*[-*]\s+([^:]+):\s*(.*)$/;
|
|
22
9
|
|
|
23
|
-
export function readAutoresearchContract(workDir: string)
|
|
10
|
+
export function readAutoresearchContract(workDir: string) {
|
|
24
11
|
const contractPath = path.join(workDir, "autoresearch.md");
|
|
25
12
|
let content = "";
|
|
26
13
|
try {
|
|
27
14
|
content = fs.readFileSync(contractPath, "utf8");
|
|
28
15
|
} catch {
|
|
29
16
|
return {
|
|
30
|
-
contract:
|
|
17
|
+
contract: {
|
|
18
|
+
benchmark: { command: null, primaryMetric: null, metricUnit: "", direction: null, secondaryMetrics: [] },
|
|
19
|
+
scopePaths: [],
|
|
20
|
+
offLimits: [],
|
|
21
|
+
constraints: [],
|
|
22
|
+
},
|
|
31
23
|
errors: [`${contractPath} does not exist. Create it before initializing autoresearch.`],
|
|
32
24
|
path: contractPath,
|
|
33
25
|
};
|
|
@@ -48,34 +40,24 @@ export function parseAutoresearchContract(markdown: string): AutoresearchContrac
|
|
|
48
40
|
};
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
function validateAutoresearchContract(contract: AutoresearchContract): string[] {
|
|
52
44
|
const errors: string[] = [];
|
|
53
|
-
if (!contract.benchmark.command)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!contract.benchmark.primaryMetric) {
|
|
57
|
-
errors.push("Benchmark.primary metric is required in autoresearch.md.");
|
|
58
|
-
}
|
|
59
|
-
if (!contract.benchmark.direction) {
|
|
45
|
+
if (!contract.benchmark.command) errors.push("Benchmark.command is required in autoresearch.md.");
|
|
46
|
+
if (!contract.benchmark.primaryMetric) errors.push("Benchmark.primary metric is required in autoresearch.md.");
|
|
47
|
+
if (!contract.benchmark.direction)
|
|
60
48
|
errors.push("Benchmark.direction must be `lower` or `higher` in autoresearch.md.");
|
|
61
|
-
|
|
62
|
-
if (contract.scopePaths.length === 0) {
|
|
49
|
+
if (contract.scopePaths.length === 0)
|
|
63
50
|
errors.push("Files in Scope must contain at least one path in autoresearch.md.");
|
|
51
|
+
for (const p of contract.scopePaths) {
|
|
52
|
+
if (isUnsafeContractPathSpec(p)) errors.push(`Files in Scope contains an invalid path: ${p}`);
|
|
64
53
|
}
|
|
65
|
-
for (const
|
|
66
|
-
if (isUnsafeContractPathSpec(
|
|
67
|
-
errors.push(`Files in Scope contains an invalid path: ${scopePath}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
for (const offLimitsPath of contract.offLimits) {
|
|
71
|
-
if (isUnsafeContractPathSpec(offLimitsPath)) {
|
|
72
|
-
errors.push(`Off Limits contains an invalid path: ${offLimitsPath}`);
|
|
73
|
-
}
|
|
54
|
+
for (const p of contract.offLimits) {
|
|
55
|
+
if (isUnsafeContractPathSpec(p)) errors.push(`Off Limits contains an invalid path: ${p}`);
|
|
74
56
|
}
|
|
75
57
|
return errors;
|
|
76
58
|
}
|
|
77
59
|
|
|
78
|
-
export function loadAutoresearchScriptSnapshot(workDir: string)
|
|
60
|
+
export function loadAutoresearchScriptSnapshot(workDir: string) {
|
|
79
61
|
const benchmarkScriptPath = path.join(workDir, "autoresearch.sh");
|
|
80
62
|
const checksScriptPath = path.join(workDir, "autoresearch.checks.sh");
|
|
81
63
|
const errors: string[] = [];
|
|
@@ -104,16 +86,7 @@ export function loadAutoresearchScriptSnapshot(workDir: string): AutoresearchScr
|
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
export function normalizeAutoresearchList(values: readonly string[]): string[] {
|
|
107
|
-
|
|
108
|
-
const seen = new Set<string>();
|
|
109
|
-
for (const value of values) {
|
|
110
|
-
const trimmed = value.trim();
|
|
111
|
-
if (trimmed.length === 0) continue;
|
|
112
|
-
if (seen.has(trimmed)) continue;
|
|
113
|
-
seen.add(trimmed);
|
|
114
|
-
normalized.push(trimmed);
|
|
115
|
-
}
|
|
116
|
-
return normalized;
|
|
89
|
+
return [...new Set(values.map(v => v.trim()).filter(Boolean))];
|
|
117
90
|
}
|
|
118
91
|
|
|
119
92
|
export function normalizeContractPathSpec(value: string): string {
|
|
@@ -130,34 +103,16 @@ export function pathMatchesContractPath(pathValue: string, specValue: string): b
|
|
|
130
103
|
}
|
|
131
104
|
|
|
132
105
|
export function contractListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
133
|
-
|
|
134
|
-
const normalizedRight = normalizeAutoresearchList(right);
|
|
135
|
-
if (normalizedLeft.length !== normalizedRight.length) return false;
|
|
136
|
-
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
106
|
+
return arraysEqual(normalizeAutoresearchList(left), normalizeAutoresearchList(right));
|
|
137
107
|
}
|
|
138
108
|
|
|
139
109
|
export function contractPathListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
140
|
-
|
|
141
|
-
const normalizedRight = normalizeContractPathList(right);
|
|
142
|
-
if (normalizedLeft.length !== normalizedRight.length) return false;
|
|
143
|
-
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
110
|
+
return arraysEqual(normalizeContractPathList(left), normalizeContractPathList(right));
|
|
144
111
|
}
|
|
145
112
|
|
|
146
|
-
function
|
|
147
|
-
return
|
|
148
|
-
benchmark: {
|
|
149
|
-
command: null,
|
|
150
|
-
primaryMetric: null,
|
|
151
|
-
metricUnit: "",
|
|
152
|
-
direction: null,
|
|
153
|
-
secondaryMetrics: [],
|
|
154
|
-
},
|
|
155
|
-
scopePaths: [],
|
|
156
|
-
offLimits: [],
|
|
157
|
-
constraints: [],
|
|
158
|
-
};
|
|
113
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
114
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
159
115
|
}
|
|
160
|
-
|
|
161
116
|
function normalizeContractPathList(values: readonly string[]): string[] {
|
|
162
117
|
return normalizeAutoresearchList(values.map(normalizeContractPathSpec)).sort((left, right) =>
|
|
163
118
|
left.localeCompare(right),
|
|
@@ -166,39 +121,30 @@ function normalizeContractPathList(values: readonly string[]): string[] {
|
|
|
166
121
|
|
|
167
122
|
function extractSections(markdown: string): Map<string, string> {
|
|
168
123
|
const sections = new Map<string, string>();
|
|
169
|
-
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
currentHeading = headingMatch[1]?.trim().toLowerCase() ?? null;
|
|
180
|
-
currentLines = [];
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (currentHeading) {
|
|
184
|
-
currentLines.push(line);
|
|
124
|
+
let heading: string | null = null;
|
|
125
|
+
let content: string[] = [];
|
|
126
|
+
for (const line of markdown.split("\n")) {
|
|
127
|
+
const match = line.match(HEADING_REGEX);
|
|
128
|
+
if (match) {
|
|
129
|
+
if (heading) sections.set(heading, content.join("\n").trim());
|
|
130
|
+
heading = match[1]?.trim().toLowerCase() ?? null;
|
|
131
|
+
content = [];
|
|
132
|
+
} else if (heading) {
|
|
133
|
+
content.push(line);
|
|
185
134
|
}
|
|
186
135
|
}
|
|
187
|
-
|
|
188
|
-
if (currentHeading) {
|
|
189
|
-
sections.set(currentHeading, currentLines.join("\n").trim());
|
|
190
|
-
}
|
|
136
|
+
if (heading) sections.set(heading, content.join("\n").trim());
|
|
191
137
|
return sections;
|
|
192
138
|
}
|
|
193
139
|
|
|
194
|
-
function parseBenchmarkSection(section: string):
|
|
140
|
+
function parseBenchmarkSection(section: string): AutoresearchContract["benchmark"] {
|
|
195
141
|
const entries = new Map<string, string>();
|
|
196
142
|
const lines = section.split("\n");
|
|
197
143
|
for (let index = 0; index < lines.length; index += 1) {
|
|
198
144
|
const rawLine = lines[index] ?? "";
|
|
199
145
|
const match = rawLine.match(KEY_VALUE_REGEX);
|
|
200
146
|
if (!match) continue;
|
|
201
|
-
const key =
|
|
147
|
+
const key = (match[1] ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
202
148
|
let value = (match[2] ?? "").trim();
|
|
203
149
|
if (key === "secondarymetrics") {
|
|
204
150
|
const nestedItems: string[] = [];
|
|
@@ -206,27 +152,33 @@ function parseBenchmarkSection(section: string): AutoresearchBenchmarkContract {
|
|
|
206
152
|
const nestedLine = lines[nestedIndex] ?? "";
|
|
207
153
|
if (nestedLine.match(KEY_VALUE_REGEX)) break;
|
|
208
154
|
const nestedMatch = nestedLine.match(/^\s{2,}[-*]\s+(.*)$/);
|
|
209
|
-
if (!nestedMatch)
|
|
210
|
-
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
155
|
+
if (!nestedMatch && nestedLine.trim().length > 0) break;
|
|
156
|
+
if (!nestedMatch) continue;
|
|
213
157
|
nestedItems.push((nestedMatch[1] ?? "").trim());
|
|
214
158
|
index = nestedIndex;
|
|
215
159
|
}
|
|
216
|
-
if (nestedItems.length > 0)
|
|
217
|
-
value = [value, ...nestedItems].filter(Boolean).join(", ");
|
|
218
|
-
}
|
|
160
|
+
if (nestedItems.length > 0) value = [value, ...nestedItems].filter(Boolean).join(", ");
|
|
219
161
|
}
|
|
220
162
|
entries.set(key, value);
|
|
221
163
|
}
|
|
222
164
|
|
|
223
|
-
const
|
|
165
|
+
const directionRaw = entries.get("direction");
|
|
166
|
+
const direction: MetricDirection | null =
|
|
167
|
+
directionRaw === "lower" || directionRaw === "higher" ? directionRaw : null;
|
|
224
168
|
return {
|
|
225
|
-
command:
|
|
226
|
-
primaryMetric:
|
|
169
|
+
command: entries.get("command")?.trim() || null,
|
|
170
|
+
primaryMetric: entries.get("primarymetric")?.trim() || null,
|
|
227
171
|
metricUnit: entries.get("metricunit")?.trim() ?? "",
|
|
228
172
|
direction,
|
|
229
|
-
secondaryMetrics:
|
|
173
|
+
secondaryMetrics: entries.get("secondarymetrics")
|
|
174
|
+
? normalizeAutoresearchList(
|
|
175
|
+
entries
|
|
176
|
+
.get("secondarymetrics")!
|
|
177
|
+
.split(",")
|
|
178
|
+
.map(e => e.trim())
|
|
179
|
+
.filter(Boolean),
|
|
180
|
+
)
|
|
181
|
+
: [],
|
|
230
182
|
};
|
|
231
183
|
}
|
|
232
184
|
|
|
@@ -252,37 +204,32 @@ function parseListSection(section: string, normalizeItem?: (value: string) => st
|
|
|
252
204
|
}
|
|
253
205
|
items.push(line.trim());
|
|
254
206
|
}
|
|
255
|
-
if (activeItem)
|
|
256
|
-
items.push(activeItem);
|
|
257
|
-
}
|
|
207
|
+
if (activeItem) items.push(activeItem);
|
|
258
208
|
const normalizedItems = normalizeAutoresearchList(items);
|
|
259
209
|
return normalizeItem ? normalizedItems.map(normalizeItem) : normalizedItems;
|
|
260
210
|
}
|
|
261
|
-
|
|
262
|
-
function normalizeKey(value: string): string {
|
|
263
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function parseDirection(value: string | undefined): MetricDirection | null {
|
|
267
|
-
if (value === "lower" || value === "higher") return value;
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function readNullableEntry(value: string | undefined): string | null {
|
|
272
|
-
const trimmed = value?.trim() ?? "";
|
|
273
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function parseSecondaryMetrics(value: string | undefined): string[] {
|
|
277
|
-
if (!value) return [];
|
|
278
|
-
return normalizeAutoresearchList(
|
|
279
|
-
value
|
|
280
|
-
.split(",")
|
|
281
|
-
.map(entry => entry.trim())
|
|
282
|
-
.filter(Boolean),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
211
|
function isUnsafeContractPathSpec(value: string): boolean {
|
|
287
212
|
return path.posix.isAbsolute(value) || value === ".." || value.startsWith("../");
|
|
288
213
|
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Updates session fields from a validated `autoresearch.md` parse (same fields as `init_experiment`).
|
|
217
|
+
* Does not touch `name`, `currentSegment`, `results`, `bestMetric`, `confidence`, or `maxExperiments`.
|
|
218
|
+
*/
|
|
219
|
+
export function applyAutoresearchContractToExperimentState(
|
|
220
|
+
contract: AutoresearchContract,
|
|
221
|
+
state: ExperimentState,
|
|
222
|
+
): void {
|
|
223
|
+
const benchmarkContract = contract.benchmark;
|
|
224
|
+
state.metricName = benchmarkContract.primaryMetric ?? state.metricName;
|
|
225
|
+
state.metricUnit = benchmarkContract.metricUnit;
|
|
226
|
+
state.bestDirection = benchmarkContract.direction ?? "lower";
|
|
227
|
+
state.secondaryMetrics = benchmarkContract.secondaryMetrics.map(name => ({
|
|
228
|
+
name,
|
|
229
|
+
unit: inferMetricUnitFromName(name),
|
|
230
|
+
}));
|
|
231
|
+
state.benchmarkCommand = benchmarkContract.command?.trim() ?? state.benchmarkCommand;
|
|
232
|
+
state.scopePaths = [...contract.scopePaths];
|
|
233
|
+
state.offLimits = [...contract.offLimits];
|
|
234
|
+
state.constraints = [...contract.constraints];
|
|
235
|
+
}
|
|
@@ -24,9 +24,7 @@ export function createDashboardController(): DashboardController {
|
|
|
24
24
|
return {
|
|
25
25
|
clear(ctx): void {
|
|
26
26
|
clear();
|
|
27
|
-
if (ctx.hasUI)
|
|
28
|
-
ctx.ui.setWidget("autoresearch", undefined);
|
|
29
|
-
}
|
|
27
|
+
if (ctx.hasUI) ctx.ui.setWidget("autoresearch", undefined);
|
|
30
28
|
},
|
|
31
29
|
requestRender,
|
|
32
30
|
updateWidget(ctx, runtime): void {
|
|
@@ -38,9 +36,8 @@ export function createDashboardController(): DashboardController {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
ctx.ui.setWidget("autoresearch", (_tui, theme) => {
|
|
41
|
-
if (state.results.length === 0 && runtime.runningExperiment)
|
|
39
|
+
if (state.results.length === 0 && runtime.runningExperiment)
|
|
42
40
|
return new Text(renderRunningOnly(runtime, state, theme), 0, 0);
|
|
43
|
-
}
|
|
44
41
|
if (runtime.dashboardExpanded) {
|
|
45
42
|
const width = process.stdout.columns ?? 120;
|
|
46
43
|
const lines = [
|
|
@@ -70,9 +67,8 @@ export function createDashboardController(): DashboardController {
|
|
|
70
67
|
const terminalRows = process.stdout.rows ?? 40;
|
|
71
68
|
const header = renderExpandedHeader(runtime, width, theme);
|
|
72
69
|
const body = renderDashboardLines(runtime, width, theme, 0);
|
|
73
|
-
if (runtime.runningExperiment)
|
|
70
|
+
if (runtime.runningExperiment)
|
|
74
71
|
body.push(renderOverlayRunningLine(runtime, theme, width, spinnerFrame));
|
|
75
|
-
}
|
|
76
72
|
const viewportRows = Math.max(4, terminalRows - 4);
|
|
77
73
|
const maxScroll = Math.max(0, body.length - viewportRows);
|
|
78
74
|
if (scrollOffset > maxScroll) scrollOffset = maxScroll;
|
|
@@ -124,12 +120,8 @@ export function createDashboardController(): DashboardController {
|
|
|
124
120
|
|
|
125
121
|
function renderRunningOnly(runtime: AutoresearchRuntime, state: ExperimentState, theme: Theme): string {
|
|
126
122
|
const parts = [theme.fg("contentAccent", "autoresearch"), theme.fg("warning", " running...")];
|
|
127
|
-
if (state.name) {
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (runtime.runningExperiment) {
|
|
131
|
-
parts.push(theme.fg("dim", ` | ${replaceTabs(runtime.runningExperiment.command)}`));
|
|
132
|
-
}
|
|
123
|
+
if (state.name) parts.push(theme.fg("dim", ` | ${replaceTabs(state.name)}`));
|
|
124
|
+
if (runtime.runningExperiment) parts.push(theme.fg("dim", ` | ${replaceTabs(runtime.runningExperiment.command)}`));
|
|
133
125
|
return parts.join("");
|
|
134
126
|
}
|
|
135
127
|
|
|
@@ -170,20 +162,14 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
170
162
|
);
|
|
171
163
|
}
|
|
172
164
|
parts.push(theme.fg("warning", " | log_experiment required"));
|
|
173
|
-
if (!runtime.autoresearchMode)
|
|
174
|
-
parts.push(theme.fg("dim", " | mode off"));
|
|
175
|
-
}
|
|
165
|
+
if (!runtime.autoresearchMode) parts.push(theme.fg("dim", " | mode off"));
|
|
176
166
|
return parts.join("");
|
|
177
167
|
}
|
|
178
168
|
if (state.results.length === 0) {
|
|
179
169
|
const modeStatus = runtime.autoresearchMode ? "baseline pending" : "mode off";
|
|
180
170
|
const parts = [theme.fg("contentAccent", "autoresearch"), theme.fg("warning", ` ${modeStatus}`)];
|
|
181
|
-
if (state.name) {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
if (runtime.autoresearchMode) {
|
|
185
|
-
parts.push(theme.fg("dim", " | run the baseline"));
|
|
186
|
-
}
|
|
171
|
+
if (state.name) parts.push(theme.fg("dim", ` | ${replaceTabs(state.name)}`));
|
|
172
|
+
if (runtime.autoresearchMode) parts.push(theme.fg("dim", " | run the baseline"));
|
|
187
173
|
return parts.join("");
|
|
188
174
|
}
|
|
189
175
|
const current = currentResults(state.results, state.currentSegment);
|
|
@@ -223,12 +209,7 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
223
209
|
return parts.join("");
|
|
224
210
|
}
|
|
225
211
|
|
|
226
|
-
|
|
227
|
-
runtime: AutoresearchRuntime,
|
|
228
|
-
width: number,
|
|
229
|
-
theme: Theme,
|
|
230
|
-
maxRows: number,
|
|
231
|
-
): string[] {
|
|
212
|
+
function renderDashboardLines(runtime: AutoresearchRuntime, width: number, theme: Theme, maxRows: number): string[] {
|
|
232
213
|
const state = runtime.state;
|
|
233
214
|
if (state.results.length === 0) {
|
|
234
215
|
if (runtime.lastRunSummary) {
|
|
@@ -240,9 +221,7 @@ export function renderDashboardLines(
|
|
|
240
221
|
),
|
|
241
222
|
truncateToWidth("Next action: finish log_experiment before starting another run.", width),
|
|
242
223
|
];
|
|
243
|
-
if (!runtime.autoresearchMode)
|
|
244
|
-
lines.push(truncateToWidth("Mode: off", width));
|
|
245
|
-
}
|
|
224
|
+
if (!runtime.autoresearchMode) lines.push(truncateToWidth("Mode: off", width));
|
|
246
225
|
return lines;
|
|
247
226
|
}
|
|
248
227
|
if (runtime.autoresearchMode) {
|
|
@@ -287,20 +266,13 @@ export function renderDashboardLines(
|
|
|
287
266
|
),
|
|
288
267
|
);
|
|
289
268
|
}
|
|
290
|
-
if (!runtime.autoresearchMode) {
|
|
291
|
-
lines.push(truncateToWidth(`Mode: ${renderModeStatus(runtime, state)}`, width));
|
|
292
|
-
}
|
|
269
|
+
if (!runtime.autoresearchMode) lines.push(truncateToWidth(`Mode: ${renderModeStatus(runtime, state)}`, width));
|
|
293
270
|
if (best) {
|
|
294
271
|
const bestRunNumber = best.result.runNumber ?? best.index + 1;
|
|
295
272
|
let progress = `Best: ${formatNum(best.result.metric, state.metricUnit)} (#${bestRunNumber})`;
|
|
296
|
-
if (baseline !== null && baseline !== 0 && best.result.metric !== baseline)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
progress += ` ${sign}${delta.toFixed(1)}%`;
|
|
300
|
-
}
|
|
301
|
-
if (state.confidence !== null) {
|
|
302
|
-
progress += ` conf ${state.confidence.toFixed(1)}x`;
|
|
303
|
-
}
|
|
273
|
+
if (baseline !== null && baseline !== 0 && best.result.metric !== baseline)
|
|
274
|
+
progress += ` ${formatDelta(best.result.metric, baseline)}`;
|
|
275
|
+
if (state.confidence !== null) progress += ` conf ${state.confidence.toFixed(1)}x`;
|
|
304
276
|
lines.push(truncateToWidth(progress, width));
|
|
305
277
|
if (state.secondaryMetrics.length > 0) {
|
|
306
278
|
const details = state.secondaryMetrics
|
|
@@ -313,9 +285,7 @@ export function renderDashboardLines(
|
|
|
313
285
|
),
|
|
314
286
|
)
|
|
315
287
|
.filter((value): value is string => Boolean(value));
|
|
316
|
-
if (details.length > 0) {
|
|
317
|
-
lines.push(truncateToWidth(`Secondary: ${details.join(" ")}`, width));
|
|
318
|
-
}
|
|
288
|
+
if (details.length > 0) lines.push(truncateToWidth(`Secondary: ${details.join(" ")}`, width));
|
|
319
289
|
}
|
|
320
290
|
}
|
|
321
291
|
lines.push("");
|
|
@@ -323,9 +293,8 @@ export function renderDashboardLines(
|
|
|
323
293
|
lines.push(theme.fg("borderMuted", "-".repeat(Math.max(0, width - 1))));
|
|
324
294
|
|
|
325
295
|
const visible = maxRows > 0 ? current.slice(-maxRows) : current;
|
|
326
|
-
if (visible.length < current.length)
|
|
296
|
+
if (visible.length < current.length)
|
|
327
297
|
lines.push(theme.fg("dim", `... ${current.length - visible.length} earlier runs hidden ...`));
|
|
328
|
-
}
|
|
329
298
|
for (const result of visible) {
|
|
330
299
|
lines.push(renderResultRow(result, state, baselineSecondary, width, theme));
|
|
331
300
|
}
|
|
@@ -358,12 +327,12 @@ function renderResultRow(
|
|
|
358
327
|
.join("");
|
|
359
328
|
const statusColor = result.status === "keep" ? "success" : result.status === "discard" ? "warning" : "error";
|
|
360
329
|
const line =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
330
|
+
theme.fg("dim", String(runNumber).padEnd(4)) +
|
|
331
|
+
theme.fg("contentAccent", (result.commit || "-").padEnd(10)) +
|
|
332
|
+
theme.fg(statusColor, formatNum(result.metric, state.metricUnit).padEnd(12)) +
|
|
333
|
+
secondary +
|
|
334
|
+
theme.fg(statusColor, result.status.padEnd(14)) +
|
|
335
|
+
theme.fg("muted", replaceTabs(result.description));
|
|
367
336
|
return truncateToWidth(line, width);
|
|
368
337
|
}
|
|
369
338
|
|
|
@@ -371,9 +340,7 @@ function renderSecondaryCell(value: number | undefined, unit: string, baseline:
|
|
|
371
340
|
if (value === undefined) return "-";
|
|
372
341
|
const formatted = formatNum(value, unit);
|
|
373
342
|
if (baseline === undefined || baseline === 0 || baseline === value) return formatted;
|
|
374
|
-
|
|
375
|
-
const sign = delta > 0 ? "+" : "";
|
|
376
|
-
return `${formatted} ${sign}${delta.toFixed(1)}%`;
|
|
343
|
+
return `${formatted} ${formatDelta(value, baseline)}`;
|
|
377
344
|
}
|
|
378
345
|
|
|
379
346
|
function renderSecondarySummary(
|
|
@@ -383,12 +350,12 @@ function renderSecondarySummary(
|
|
|
383
350
|
unit: string,
|
|
384
351
|
): string | null {
|
|
385
352
|
if (value === undefined) return null;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
353
|
+
return `${name} ${renderSecondaryCell(value, unit, baseline)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatDelta(value: number, baseline: number): string {
|
|
389
357
|
const delta = ((value - baseline) / baseline) * 100;
|
|
390
|
-
|
|
391
|
-
return `${name} ${formatNum(value, unit)} ${sign}${delta.toFixed(1)}%`;
|
|
358
|
+
return `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%`;
|
|
392
359
|
}
|
|
393
360
|
|
|
394
361
|
function renderOverlayRunningLine(
|
|
@@ -426,13 +393,9 @@ function renderOverlayFooter(
|
|
|
426
393
|
}
|
|
427
394
|
|
|
428
395
|
function renderModeStatus(runtime: AutoresearchRuntime, state: ExperimentState): string {
|
|
429
|
-
if (runtime.autoresearchMode)
|
|
430
|
-
return state.results.length === 0 ? "baseline pending" : "mode on";
|
|
431
|
-
}
|
|
396
|
+
if (runtime.autoresearchMode) return state.results.length === 0 ? "baseline pending" : "mode on";
|
|
432
397
|
const current = currentResults(state.results, state.currentSegment);
|
|
433
|
-
if (state.maxExperiments !== null && current.length >= state.maxExperiments)
|
|
434
|
-
return "segment complete";
|
|
435
|
-
}
|
|
398
|
+
if (state.maxExperiments !== null && current.length >= state.maxExperiments) return "segment complete";
|
|
436
399
|
return "mode off";
|
|
437
400
|
}
|
|
438
401
|
|
|
@@ -441,9 +404,7 @@ function findBestResult(state: ExperimentState): { index: number; result: Experi
|
|
|
441
404
|
for (let index = 0; index < state.results.length; index += 1) {
|
|
442
405
|
const result = state.results[index];
|
|
443
406
|
if (result.segment !== state.currentSegment || result.status !== "keep" || result.metric <= 0) continue;
|
|
444
|
-
if (!best || isBetter(result.metric, best.result.metric, state.bestDirection)) {
|
|
445
|
-
best = { index, result };
|
|
446
|
-
}
|
|
407
|
+
if (!best || isBetter(result.metric, best.result.metric, state.bestDirection)) best = { index, result };
|
|
447
408
|
}
|
|
448
409
|
return best;
|
|
449
410
|
}
|