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/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
- const MUTATION_KEYS = ["r", "s", "m", "return"];
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.views = [createFleetView(), createRoutingView(), createPreviewView(), createProfileView()];
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", () => this.setView((this.active + 1) % this.views.length));
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), () => this.setView(idx));
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.showingHelp = true;
61
- this.paint();
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
- this.engine.key("q", () => this.stop());
74
- // mutation keys — read-only until the v1.67 write path
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, () => this.showNotice());
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-4 switch view",
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 (read-only)",
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;
@@ -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;