tickmarkr 1.66.0 → 1.68.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/dist/cli/commands/run.js +9 -0
- package/dist/cli/commands/status.d.ts +20 -0
- package/dist/cli/commands/status.js +41 -27
- package/dist/compile/collateral.d.ts +1 -0
- package/dist/compile/collateral.js +52 -3
- package/dist/gates/run-gates.js +13 -6
- package/dist/run/daemon.js +26 -2
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +9 -0
- package/dist/tui/app.d.ts +32 -0
- package/dist/tui/app.js +223 -19
- package/dist/tui/save.d.ts +38 -0
- package/dist/tui/save.js +96 -0
- package/dist/tui/staging.d.ts +29 -0
- package/dist/tui/staging.js +78 -0
- package/dist/tui/views/consult-dossier.d.ts +49 -0
- package/dist/tui/views/consult-dossier.js +169 -0
- package/dist/tui/views/diff-modal.d.ts +9 -0
- package/dist/tui/views/diff-modal.js +19 -0
- package/dist/tui/views/fleet-view.d.ts +23 -1
- package/dist/tui/views/fleet-view.js +108 -14
- package/dist/tui/views/routing-view.d.ts +22 -3
- package/dist/tui/views/routing-view.js +152 -16
- package/dist/tui/views/runs-view.d.ts +70 -0
- package/dist/tui/views/runs-view.js +387 -0
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -3,14 +3,34 @@ import { createFleetView } from "./views/fleet-view.js";
|
|
|
3
3
|
import { createPreviewView } from "./views/preview-view.js";
|
|
4
4
|
import { createProfileView } from "./views/profile-view.js";
|
|
5
5
|
import { createRoutingView } from "./views/routing-view.js";
|
|
6
|
-
|
|
6
|
+
import { createRunsView } from "./views/runs-view.js";
|
|
7
|
+
import { renderDiffModal } from "./views/diff-modal.js";
|
|
8
|
+
import { buildSaveProposal, confirmSave } from "./save.js";
|
|
9
|
+
import { FleetStaging } from "./staging.js";
|
|
10
|
+
const MUTATION_KEYS = ["r", "m", "return"];
|
|
7
11
|
const READONLY_NOTICE = "read-only in v1.66 — write path ships in v1.67";
|
|
12
|
+
function emptyFleetEditable() {
|
|
13
|
+
return {
|
|
14
|
+
denyAdapters: [],
|
|
15
|
+
denyModels: [],
|
|
16
|
+
tiers: {},
|
|
17
|
+
map: {},
|
|
18
|
+
floors: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
8
21
|
export class StudioApp {
|
|
9
22
|
engine;
|
|
10
23
|
views;
|
|
11
24
|
active = 0;
|
|
12
25
|
showingHelp = false;
|
|
26
|
+
confirmingQuit = false;
|
|
27
|
+
showingSave = false;
|
|
28
|
+
saveTarget = "repo";
|
|
29
|
+
saveProposal = null;
|
|
13
30
|
notice = null;
|
|
31
|
+
staging;
|
|
32
|
+
repoRoot;
|
|
33
|
+
globalDir;
|
|
14
34
|
_lines = [];
|
|
15
35
|
resolveExit = null;
|
|
16
36
|
exited;
|
|
@@ -20,7 +40,10 @@ export class StudioApp {
|
|
|
20
40
|
output: opts.output,
|
|
21
41
|
onResize: () => this.paint(),
|
|
22
42
|
});
|
|
23
|
-
this.
|
|
43
|
+
this.repoRoot = opts.repoRoot ?? process.cwd();
|
|
44
|
+
this.globalDir = opts.globalDir;
|
|
45
|
+
this.staging = new FleetStaging(opts.loaded ?? emptyFleetEditable());
|
|
46
|
+
this.views = [createFleetView(), createRoutingView(), createPreviewView(), createProfileView(), createRunsView()];
|
|
24
47
|
this.exited = new Promise((resolve) => {
|
|
25
48
|
this.resolveExit = resolve;
|
|
26
49
|
});
|
|
@@ -47,33 +70,186 @@ export class StudioApp {
|
|
|
47
70
|
this.resolveExit = null;
|
|
48
71
|
}
|
|
49
72
|
}
|
|
73
|
+
/** Apply an edit to the staging buffer and repaint. Intended as the test seam and future edit hook. */
|
|
74
|
+
stageEdit(edit) {
|
|
75
|
+
this.staging.apply(edit);
|
|
76
|
+
this.paint();
|
|
77
|
+
}
|
|
78
|
+
get isModalOpen() {
|
|
79
|
+
return this.showingHelp || this.confirmingQuit || this.showingSave;
|
|
80
|
+
}
|
|
50
81
|
bindKeys() {
|
|
51
82
|
// view switching
|
|
52
83
|
// Tab is decoded as the raw "\t" sequence by the micro-engine.
|
|
53
|
-
this.engine.key("\t", () =>
|
|
84
|
+
this.engine.key("\t", () => {
|
|
85
|
+
if (!this.isModalOpen)
|
|
86
|
+
this.setView((this.active + 1) % this.views.length);
|
|
87
|
+
});
|
|
54
88
|
for (let i = 1; i <= this.views.length; i++) {
|
|
55
89
|
const idx = i - 1;
|
|
56
|
-
this.engine.key(String(i), () =>
|
|
90
|
+
this.engine.key(String(i), () => {
|
|
91
|
+
if (!this.isModalOpen)
|
|
92
|
+
this.setView(idx);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// per-view navigation
|
|
96
|
+
for (const name of ["up", "down"]) {
|
|
97
|
+
this.engine.key(name, () => {
|
|
98
|
+
if (!this.isModalOpen)
|
|
99
|
+
this.forwardViewKey(name);
|
|
100
|
+
});
|
|
57
101
|
}
|
|
58
102
|
// help overlay
|
|
59
103
|
this.engine.key("?", () => {
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
// dismissal / quit
|
|
64
|
-
this.engine.key("escape", () => {
|
|
65
|
-
if (this.showingHelp) {
|
|
66
|
-
this.showingHelp = false;
|
|
104
|
+
if (!this.confirmingQuit && !this.showingSave) {
|
|
105
|
+
this.showingHelp = true;
|
|
67
106
|
this.paint();
|
|
68
107
|
}
|
|
69
|
-
else {
|
|
70
|
-
this.stop();
|
|
71
|
-
}
|
|
72
108
|
});
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
// dismissal / quit
|
|
110
|
+
this.engine.key("escape", () => this.handleEscape());
|
|
111
|
+
this.engine.key("q", () => this.handleQuit());
|
|
112
|
+
// revert staged changes
|
|
113
|
+
this.engine.key("u", () => this.handleRevert());
|
|
114
|
+
// save modal
|
|
115
|
+
this.engine.key("s", () => this.handleSaveOpen());
|
|
116
|
+
this.engine.key("t", () => this.handleSaveToggle());
|
|
117
|
+
// quit-confirmation / save-confirmation answers
|
|
118
|
+
this.engine.key("y", () => this.handleConfirm());
|
|
119
|
+
this.engine.key("n", () => this.handleCancel());
|
|
120
|
+
// remaining mutation keys — read-only until wired
|
|
75
121
|
for (const key of MUTATION_KEYS) {
|
|
76
|
-
this.engine.key(key, () =>
|
|
122
|
+
this.engine.key(key, () => {
|
|
123
|
+
if (!this.isModalOpen)
|
|
124
|
+
this.showNotice();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
handleEscape() {
|
|
129
|
+
if (this.showingHelp) {
|
|
130
|
+
this.showingHelp = false;
|
|
131
|
+
this.paint();
|
|
132
|
+
}
|
|
133
|
+
else if (this.showingSave) {
|
|
134
|
+
this.closeSave(false);
|
|
135
|
+
}
|
|
136
|
+
else if (this.confirmingQuit) {
|
|
137
|
+
this.confirmingQuit = false;
|
|
138
|
+
this.paint();
|
|
139
|
+
}
|
|
140
|
+
else if (this.staging.isDirty) {
|
|
141
|
+
this.confirmingQuit = true;
|
|
142
|
+
this.paint();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.stop();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
handleQuit() {
|
|
149
|
+
if (this.showingHelp || this.showingSave)
|
|
150
|
+
return; // modals block q
|
|
151
|
+
if (this.confirmingQuit) {
|
|
152
|
+
this.stop();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (this.staging.isDirty) {
|
|
156
|
+
this.confirmingQuit = true;
|
|
157
|
+
this.paint();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this.stop();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
handleSaveOpen() {
|
|
164
|
+
if (this.isModalOpen)
|
|
165
|
+
return;
|
|
166
|
+
if (!this.staging.isDirty) {
|
|
167
|
+
this.notice = "no staged changes";
|
|
168
|
+
this.paint();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.showingSave = true;
|
|
172
|
+
this.saveTarget = "repo";
|
|
173
|
+
this.saveProposal = buildSaveProposal({
|
|
174
|
+
repoRoot: this.repoRoot,
|
|
175
|
+
loaded: this.staging.loadedState,
|
|
176
|
+
staged: this.staging.current,
|
|
177
|
+
target: this.saveTarget,
|
|
178
|
+
globalDir: this.globalDir,
|
|
179
|
+
});
|
|
180
|
+
this.paint();
|
|
181
|
+
}
|
|
182
|
+
handleSaveToggle() {
|
|
183
|
+
if (!this.showingSave)
|
|
184
|
+
return;
|
|
185
|
+
this.saveTarget = this.saveTarget === "repo" ? "global" : "repo";
|
|
186
|
+
this.saveProposal = buildSaveProposal({
|
|
187
|
+
repoRoot: this.repoRoot,
|
|
188
|
+
loaded: this.staging.loadedState,
|
|
189
|
+
staged: this.staging.current,
|
|
190
|
+
target: this.saveTarget,
|
|
191
|
+
globalDir: this.globalDir,
|
|
192
|
+
});
|
|
193
|
+
this.paint();
|
|
194
|
+
}
|
|
195
|
+
handleConfirm() {
|
|
196
|
+
if (this.showingSave) {
|
|
197
|
+
this.executeSave();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.confirmingQuit) {
|
|
201
|
+
this.stop();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
handleCancel() {
|
|
205
|
+
if (this.showingSave) {
|
|
206
|
+
this.closeSave(false);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (this.confirmingQuit) {
|
|
210
|
+
this.confirmingQuit = false;
|
|
211
|
+
this.paint();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
executeSave() {
|
|
215
|
+
if (!this.saveProposal)
|
|
216
|
+
return;
|
|
217
|
+
const result = confirmSave({
|
|
218
|
+
repoRoot: this.repoRoot,
|
|
219
|
+
loaded: this.staging.loadedState,
|
|
220
|
+
staged: this.staging.current,
|
|
221
|
+
target: this.saveTarget,
|
|
222
|
+
globalDir: this.globalDir,
|
|
223
|
+
});
|
|
224
|
+
if (result.kind === "written") {
|
|
225
|
+
this.staging = new FleetStaging(this.staging.current);
|
|
226
|
+
this.closeSave(true, `wrote ${result.path}`);
|
|
227
|
+
}
|
|
228
|
+
else if (result.kind === "refused") {
|
|
229
|
+
this.closeSave(true, `save refused: ${result.reason}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.closeSave(false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
closeSave(notify, message) {
|
|
236
|
+
this.showingSave = false;
|
|
237
|
+
this.saveProposal = null;
|
|
238
|
+
if (notify && message)
|
|
239
|
+
this.notice = message;
|
|
240
|
+
this.paint();
|
|
241
|
+
}
|
|
242
|
+
handleRevert() {
|
|
243
|
+
if (this.isModalOpen)
|
|
244
|
+
return;
|
|
245
|
+
if (this.staging.isDirty) {
|
|
246
|
+
this.staging.revert();
|
|
247
|
+
this.notice = null;
|
|
248
|
+
this.paint();
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.notice = "no staged changes";
|
|
252
|
+
this.paint();
|
|
77
253
|
}
|
|
78
254
|
}
|
|
79
255
|
setView(idx) {
|
|
@@ -82,6 +258,10 @@ export class StudioApp {
|
|
|
82
258
|
this.notice = null;
|
|
83
259
|
this.paint();
|
|
84
260
|
}
|
|
261
|
+
forwardViewKey(name) {
|
|
262
|
+
this.views[this.active]?.key?.(name);
|
|
263
|
+
this.paint();
|
|
264
|
+
}
|
|
85
265
|
showNotice() {
|
|
86
266
|
this.notice = READONLY_NOTICE;
|
|
87
267
|
this.paint();
|
|
@@ -97,6 +277,17 @@ export class StudioApp {
|
|
|
97
277
|
if (this.showingHelp) {
|
|
98
278
|
lines.push(...this.renderHelp());
|
|
99
279
|
}
|
|
280
|
+
else if (this.showingSave && this.saveProposal) {
|
|
281
|
+
lines.push(...renderDiffModal({
|
|
282
|
+
target: this.saveProposal.target,
|
|
283
|
+
targetPath: this.saveProposal.targetPath,
|
|
284
|
+
diff: this.saveProposal.diff,
|
|
285
|
+
liveRun: this.saveProposal.liveRun,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
else if (this.confirmingQuit) {
|
|
289
|
+
lines.push(...this.renderQuitConfirm());
|
|
290
|
+
}
|
|
100
291
|
else {
|
|
101
292
|
const view = this.views[this.active];
|
|
102
293
|
lines.push(...view.render({ cols, rows: Math.max(rows - 2, 1) }));
|
|
@@ -104,6 +295,7 @@ export class StudioApp {
|
|
|
104
295
|
if (this.notice) {
|
|
105
296
|
lines.push(this.notice);
|
|
106
297
|
}
|
|
298
|
+
lines.push(this.renderStatusBar());
|
|
107
299
|
return lines;
|
|
108
300
|
}
|
|
109
301
|
renderTabBar() {
|
|
@@ -115,16 +307,28 @@ export class StudioApp {
|
|
|
115
307
|
}
|
|
116
308
|
return parts.join(" ");
|
|
117
309
|
}
|
|
310
|
+
renderStatusBar() {
|
|
311
|
+
if (this.staging.isDirty) {
|
|
312
|
+
const n = this.staging.changeCount;
|
|
313
|
+
return `● ${n} staged change${n === 1 ? "" : "s"}`;
|
|
314
|
+
}
|
|
315
|
+
return "no staged changes";
|
|
316
|
+
}
|
|
118
317
|
renderHelp() {
|
|
119
318
|
return [
|
|
120
319
|
"Key bindings",
|
|
121
|
-
"1-
|
|
320
|
+
"1-5 switch view",
|
|
122
321
|
"tab cycle views",
|
|
123
322
|
"? show this help",
|
|
124
323
|
"esc / q quit",
|
|
324
|
+
"u revert staged changes",
|
|
125
325
|
"r re-probe (read-only)",
|
|
126
|
-
"s save
|
|
326
|
+
"s save",
|
|
127
327
|
"m mode (read-only)",
|
|
128
328
|
];
|
|
129
329
|
}
|
|
330
|
+
renderQuitConfirm() {
|
|
331
|
+
const n = this.staging.changeCount;
|
|
332
|
+
return [`Quit with ${n} staged change${n === 1 ? "" : "s"}? (y/n)`];
|
|
333
|
+
}
|
|
130
334
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FleetEditable } from "../config/config.js";
|
|
2
|
+
export type SaveTarget = "repo" | "global";
|
|
3
|
+
/** Notice surfaced in the save modal whenever a run lock is live. */
|
|
4
|
+
export declare const RELOAD_GUARD_NOTICE = "reload guard: a live run is active; the running daemon will not see these changes until it reloads";
|
|
5
|
+
export type SaveProposal = {
|
|
6
|
+
target: SaveTarget;
|
|
7
|
+
targetPath: string;
|
|
8
|
+
before: string;
|
|
9
|
+
after: string;
|
|
10
|
+
diff: string;
|
|
11
|
+
liveRun: boolean;
|
|
12
|
+
loadError: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type SaveResult = {
|
|
15
|
+
kind: "written";
|
|
16
|
+
path: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "declined";
|
|
19
|
+
} | {
|
|
20
|
+
kind: "refused";
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
/** Build the diff-confirm payload for the current staging delta and chosen target. */
|
|
24
|
+
export declare function buildSaveProposal(opts: {
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
loaded: FleetEditable;
|
|
27
|
+
staged: FleetEditable;
|
|
28
|
+
target: SaveTarget;
|
|
29
|
+
globalDir?: string;
|
|
30
|
+
}): SaveProposal;
|
|
31
|
+
/** Confirm and execute the write for the chosen target. */
|
|
32
|
+
export declare function confirmSave(opts: {
|
|
33
|
+
repoRoot: string;
|
|
34
|
+
loaded: FleetEditable;
|
|
35
|
+
staged: FleetEditable;
|
|
36
|
+
target: SaveTarget;
|
|
37
|
+
globalDir?: string;
|
|
38
|
+
}): SaveResult;
|
package/dist/tui/save.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, repoOverlayYaml, unifiedYamlDiff, } from "../config/config.js";
|
|
6
|
+
import { isRunLockLive } from "../run/lock.js";
|
|
7
|
+
/** Notice surfaced in the save modal whenever a run lock is live. */
|
|
8
|
+
export const RELOAD_GUARD_NOTICE = "reload guard: a live run is active; the running daemon will not see these changes until it reloads";
|
|
9
|
+
function overlayPath(repoRoot, target, globalDir) {
|
|
10
|
+
return target === "repo"
|
|
11
|
+
? repoOverlayPath(repoRoot)
|
|
12
|
+
: join(globalDir ?? globalConfigDir(), "config.yaml");
|
|
13
|
+
}
|
|
14
|
+
function currentOverlayText(path) {
|
|
15
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
16
|
+
}
|
|
17
|
+
function computeAfterBytes(target, loaded, staged, repoRoot, globalDir) {
|
|
18
|
+
const path = overlayPath(repoRoot, target, globalDir);
|
|
19
|
+
const existing = readOverlayFile(path);
|
|
20
|
+
// Reuse the existing fleet serializer and diff renderer rather than inventing a parallel formatter.
|
|
21
|
+
const merged = fleetRepoOverlayFromDelta(loaded, staged, existing);
|
|
22
|
+
return repoOverlayYaml(merged);
|
|
23
|
+
}
|
|
24
|
+
function roundTripError(target, repoRoot, after, staged, globalDir) {
|
|
25
|
+
try {
|
|
26
|
+
let effective;
|
|
27
|
+
if (target === "repo") {
|
|
28
|
+
effective = loadConfigWithMode(repoRoot, { globalDir, repoOverlayText: after });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Validate a candidate global overlay by loading with it in a scratch globalDir.
|
|
32
|
+
const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-global-rt-"));
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(join(scratch, "config.yaml"), after);
|
|
35
|
+
effective = loadConfigWithMode(repoRoot, { globalDir: scratch });
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
try {
|
|
39
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
catch { /* best-effort cleanup */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const reparsed = fleetEditableFromConfig(effective.cfg);
|
|
45
|
+
if (!fleetEditableEquals(reparsed, staged)) {
|
|
46
|
+
return "round-trip mismatch: the written overlay does not re-parse to the staged state";
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return e.message;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Build the diff-confirm payload for the current staging delta and chosen target. */
|
|
55
|
+
export function buildSaveProposal(opts) {
|
|
56
|
+
const path = overlayPath(opts.repoRoot, opts.target, opts.globalDir);
|
|
57
|
+
const before = currentOverlayText(path);
|
|
58
|
+
const after = computeAfterBytes(opts.target, opts.loaded, opts.staged, opts.repoRoot, opts.globalDir);
|
|
59
|
+
const diff = unifiedYamlDiff(before, after, path);
|
|
60
|
+
const loadError = roundTripError(opts.target, opts.repoRoot, after, opts.staged, opts.globalDir);
|
|
61
|
+
return {
|
|
62
|
+
target: opts.target,
|
|
63
|
+
targetPath: path,
|
|
64
|
+
before,
|
|
65
|
+
after,
|
|
66
|
+
diff,
|
|
67
|
+
liveRun: isRunLockLive(opts.repoRoot),
|
|
68
|
+
loadError,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function atomicWrite(path, bytes) {
|
|
72
|
+
const dir = dirname(path);
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
// The sibling temp keeps rename(2) on one filesystem: readers see old or new bytes, never a partial overlay.
|
|
75
|
+
const tmp = join(dir, `.tickmarkr-save-${randomUUID()}.tmp`);
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(tmp, bytes);
|
|
78
|
+
renameSync(tmp, path);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
rmSync(tmp, { force: true }); // a failed rename must not leave a partial candidate behind
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Confirm and execute the write for the chosen target. */
|
|
86
|
+
export function confirmSave(opts) {
|
|
87
|
+
const proposal = buildSaveProposal(opts);
|
|
88
|
+
if (proposal.loadError) {
|
|
89
|
+
return { kind: "refused", reason: proposal.loadError };
|
|
90
|
+
}
|
|
91
|
+
if (proposal.before === proposal.after) {
|
|
92
|
+
return { kind: "declined" };
|
|
93
|
+
}
|
|
94
|
+
atomicWrite(proposal.targetPath, proposal.after);
|
|
95
|
+
return { kind: "written", path: proposal.targetPath };
|
|
96
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FleetEditable } from "../config/config.js";
|
|
2
|
+
/** One in-memory edit to the buffered fleet state. */
|
|
3
|
+
export type FleetEdit = (buffer: FleetEditable) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Staged-changes buffer for the Fleet Studio write path.
|
|
6
|
+
*
|
|
7
|
+
* - The loaded state is captured at construction and never mutated.
|
|
8
|
+
* - Edits apply to an in-memory buffer only.
|
|
9
|
+
* - `revert()` discards the buffer and restores the loaded state.
|
|
10
|
+
* - The delta is computed through the existing `fleetEditableEquals` comparison.
|
|
11
|
+
* - No code path in this module writes to disk.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FleetStaging {
|
|
14
|
+
private readonly loaded;
|
|
15
|
+
private buffer;
|
|
16
|
+
constructor(loaded: FleetEditable);
|
|
17
|
+
/** Read-only snapshot of the originally loaded state. */
|
|
18
|
+
get loadedState(): FleetEditable;
|
|
19
|
+
/** Current buffered (possibly edited) state. */
|
|
20
|
+
get current(): FleetEditable;
|
|
21
|
+
/** Apply an edit to the buffer. Never touches disk. */
|
|
22
|
+
apply(edit: FleetEdit): void;
|
|
23
|
+
/** Discard all buffered edits and restore the loaded state. */
|
|
24
|
+
revert(): void;
|
|
25
|
+
/** True when the buffer differs from the loaded state. */
|
|
26
|
+
get isDirty(): boolean;
|
|
27
|
+
/** Number of distinct staged changes vs the loaded state. */
|
|
28
|
+
get changeCount(): number;
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fleetEditableEquals } from "../config/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Staged-changes buffer for the Fleet Studio write path.
|
|
4
|
+
*
|
|
5
|
+
* - The loaded state is captured at construction and never mutated.
|
|
6
|
+
* - Edits apply to an in-memory buffer only.
|
|
7
|
+
* - `revert()` discards the buffer and restores the loaded state.
|
|
8
|
+
* - The delta is computed through the existing `fleetEditableEquals` comparison.
|
|
9
|
+
* - No code path in this module writes to disk.
|
|
10
|
+
*/
|
|
11
|
+
export class FleetStaging {
|
|
12
|
+
loaded;
|
|
13
|
+
buffer;
|
|
14
|
+
constructor(loaded) {
|
|
15
|
+
this.loaded = structuredClone(loaded);
|
|
16
|
+
this.buffer = structuredClone(loaded);
|
|
17
|
+
}
|
|
18
|
+
/** Read-only snapshot of the originally loaded state. */
|
|
19
|
+
get loadedState() {
|
|
20
|
+
return structuredClone(this.loaded);
|
|
21
|
+
}
|
|
22
|
+
/** Current buffered (possibly edited) state. */
|
|
23
|
+
get current() {
|
|
24
|
+
return structuredClone(this.buffer);
|
|
25
|
+
}
|
|
26
|
+
/** Apply an edit to the buffer. Never touches disk. */
|
|
27
|
+
apply(edit) {
|
|
28
|
+
edit(this.buffer);
|
|
29
|
+
}
|
|
30
|
+
/** Discard all buffered edits and restore the loaded state. */
|
|
31
|
+
revert() {
|
|
32
|
+
this.buffer = structuredClone(this.loaded);
|
|
33
|
+
}
|
|
34
|
+
/** True when the buffer differs from the loaded state. */
|
|
35
|
+
get isDirty() {
|
|
36
|
+
return !fleetEditableEquals(this.loaded, this.buffer);
|
|
37
|
+
}
|
|
38
|
+
/** Number of distinct staged changes vs the loaded state. */
|
|
39
|
+
get changeCount() {
|
|
40
|
+
return fleetEditableDeltaCount(this.loaded, this.buffer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Count changed leaves using the same equality semantics as `fleetEditableEquals`. */
|
|
44
|
+
function fleetEditableDeltaCount(initial, edited) {
|
|
45
|
+
let count = 0;
|
|
46
|
+
if (JSON.stringify(initial.denyAdapters) !== JSON.stringify(edited.denyAdapters)) {
|
|
47
|
+
count += 1;
|
|
48
|
+
}
|
|
49
|
+
if (JSON.stringify(initial.denyModels) !== JSON.stringify(edited.denyModels)) {
|
|
50
|
+
count += 1;
|
|
51
|
+
}
|
|
52
|
+
const mapKeys = new Set([...Object.keys(initial.map), ...Object.keys(edited.map)]);
|
|
53
|
+
for (const shape of mapKeys) {
|
|
54
|
+
if (JSON.stringify(initial.map[shape]) !== JSON.stringify(edited.map[shape])) {
|
|
55
|
+
count += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const floorKeys = new Set([...Object.keys(initial.floors), ...Object.keys(edited.floors)]);
|
|
59
|
+
for (const shape of floorKeys) {
|
|
60
|
+
if (initial.floors[shape] !== edited.floors[shape]) {
|
|
61
|
+
count += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const adapterKeys = new Set([...Object.keys(initial.tiers), ...Object.keys(edited.tiers)]);
|
|
65
|
+
for (const adapter of adapterKeys) {
|
|
66
|
+
const modelKeys = new Set([
|
|
67
|
+
...Object.keys(initial.tiers[adapter] ?? {}),
|
|
68
|
+
...Object.keys(edited.tiers[adapter] ?? {}),
|
|
69
|
+
]);
|
|
70
|
+
for (const model of modelKeys) {
|
|
71
|
+
if (JSON.stringify(initial.tiers[adapter]?.[model]) !==
|
|
72
|
+
JSON.stringify(edited.tiers[adapter]?.[model])) {
|
|
73
|
+
count += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConsultVerdict } from "../../run/consult.js";
|
|
2
|
+
import type { JournalEvent } from "../../run/journal.js";
|
|
3
|
+
/** Placeholder marker surfaced in the Runs detail panel until T2 fills the viewer. */
|
|
4
|
+
export declare const DOSSIER_PLACEHOLDER = "consult dossier viewer \u2014 stub, filled by T2";
|
|
5
|
+
/** Placeholder renderer — returns one dim line. */
|
|
6
|
+
export declare function renderDossierPlaceholder(): string;
|
|
7
|
+
export type ConsultAction = ConsultVerdict["action"];
|
|
8
|
+
/** One recorded consult verdict, with its persisted prompt content injected by the caller. */
|
|
9
|
+
export interface DossierVerdict {
|
|
10
|
+
action: ConsultAction;
|
|
11
|
+
reason?: string;
|
|
12
|
+
notes?: string;
|
|
13
|
+
guidance?: string;
|
|
14
|
+
/** Persisted prompt content (the consults/<task>-<n>.md artifact), loaded by the caller. */
|
|
15
|
+
prompt?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Everything the dossier panel renders — loaded by the caller, never by the render path. */
|
|
18
|
+
export interface ConsultDossierData {
|
|
19
|
+
taskId: string;
|
|
20
|
+
/** Verdicts in dispatch order (journal order). */
|
|
21
|
+
verdicts: DossierVerdict[];
|
|
22
|
+
}
|
|
23
|
+
export interface ConsultDossierPanel {
|
|
24
|
+
/** Change the selected task. Any selection change invalidates every cached prompt render. */
|
|
25
|
+
select(data: ConsultDossierData | undefined): void;
|
|
26
|
+
/** "up"/"down" move the verdict cursor; "enter" toggles the selected verdict's dossier. */
|
|
27
|
+
key(name: string): void;
|
|
28
|
+
/** Render the panel body lines (chrome/divider lines are the host view's job). */
|
|
29
|
+
render(): string[];
|
|
30
|
+
/** Index of the expanded verdict, or null when collapsed. */
|
|
31
|
+
readonly expandedIndex: number | null;
|
|
32
|
+
}
|
|
33
|
+
/** Fold one task's consult-verdict journal events into dossier verdicts, in dispatch order.
|
|
34
|
+
* Pure function over injected events. opts.prompts carries the persisted prompt contents in
|
|
35
|
+
* the same dispatch order (the caller reads the consults/ artifacts); a verdict whose artifact
|
|
36
|
+
* was not loaded simply has no prompt. Malformed events are skipped, never fatal. */
|
|
37
|
+
export declare function foldConsultVerdicts(events: JournalEvent[], taskId: string, opts?: {
|
|
38
|
+
prompts?: string[];
|
|
39
|
+
}): DossierVerdict[];
|
|
40
|
+
/** Translate persisted prompt content into styled terminal lines. Pure. */
|
|
41
|
+
export declare function translatePromptContent(content: string): string[];
|
|
42
|
+
/** A verdict's guidance field is "imperative steps for the worker (newline-separated ok)" —
|
|
43
|
+
* already list-shaped, so it renders as a bullet list, never a wall of prose. Pure. */
|
|
44
|
+
export declare function renderGuidanceSteps(guidance: string): string[];
|
|
45
|
+
/** The consult dossier panel. Stateful over injected data: a verdict cursor, at most one
|
|
46
|
+
* expanded dossier, and a per-verdict render cache cleared only when the selection changes. */
|
|
47
|
+
export declare function createConsultDossierPanel(initial?: ConsultDossierData, opts?: {
|
|
48
|
+
translate?: (content: string) => string[];
|
|
49
|
+
}): ConsultDossierPanel;
|