@underactive/pi-topping-moa-fusion 0.1.0 → 0.3.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 +54 -0
- package/README.md +54 -12
- package/agents/moa-synthesizer.md +9 -6
- package/package.json +5 -5
- package/src/config/planName.ts +29 -24
- package/src/config/settings.ts +38 -1
- package/src/debate/debateFanout.ts +16 -6
- package/src/debate/runDebate.ts +13 -4
- package/src/index.ts +14 -3
- package/src/moa/conflictContract.ts +2 -1
- package/src/moa/contextContract.ts +1 -1
- package/src/moa/fanout.ts +115 -4
- package/src/moa/fanoutWiring.ts +13 -3
- package/src/moa/implementationRetry.ts +2 -2
- package/src/moa/orchestration.ts +3 -0
- package/src/moa/panelDependence.ts +87 -0
- package/src/moa/reviewLoop.ts +2 -2
- package/src/moa/synthesis.ts +12 -8
- package/src/moa/verification.ts +117 -20
- package/src/moa/verificationCriteria.ts +1 -1
- package/src/opinion/opinionFanout.ts +5 -4
- package/src/opinion/runOpinion.ts +14 -13
- package/src/planning/contextEdits.ts +62 -0
- package/src/planning/modeState.ts +17 -0
- package/src/planning/planFile.ts +30 -12
- package/src/planning/planMode.ts +144 -11
- package/src/planning/tools/exitPlanMode.ts +7 -2
- package/src/planning/tools/mfPlanSubagent.ts +3 -0
- package/src/planning/tools/shared.ts +1 -0
- package/src/runtime/activityTracking.ts +28 -12
- package/src/runtime/processPool.ts +2 -0
- package/src/runtime/results.ts +18 -0
- package/src/runtime/runner.ts +45 -9
- package/src/runtime/wire.ts +11 -0
- package/src/spec/runSpec.ts +343 -0
- package/src/spec/specContract.ts +324 -0
- package/src/ui/cancelOverlay.ts +1 -1
- package/src/ui/moaModelPicker.ts +32 -4
- package/src/ui/moaProgressWidget.ts +114 -12
- package/src/ui/moaSetupOverlay.ts +51 -7
- package/src/ui/planReviewOverlay.ts +16 -8
- package/src/ui/rosterEditor.ts +18 -12
- package/src/ui/workingOverlay.ts +265 -0
package/src/ui/moaModelPicker.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* starting unassigned at `(none)`, plus a Load Roster row that applies a saved
|
|
9
9
|
* roster to every slot wholesale, and a Start fan-out action. Enter on a slot
|
|
10
10
|
* opens its two-pane model/thinking picker; confirming writes only that slot
|
|
11
|
-
* and returns to the overview, cancelling returns without changing it.
|
|
11
|
+
* and returns to the overview, cancelling returns without changing it.
|
|
12
|
+
* Backspace or Delete on a slot clears it back to `(none)`. The
|
|
12
13
|
* overview's "Start fan-out" action stays visually disabled until at least two
|
|
13
14
|
* proposer slots and all three required roles are assigned, then it finishes
|
|
14
15
|
* the picker.
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
import { PLAN_OVERLAY_OPTIONS } from "./menu.ts";
|
|
45
46
|
import { TwoPaneModelThinking } from "./twoPaneModelThinking.ts";
|
|
46
47
|
import { smartTruncateModelLabel } from "./modelLabel.ts";
|
|
48
|
+
import { proposerDiversityWarning } from "../moa/panelDependence.ts";
|
|
47
49
|
import { getModelCatalogue } from "../config/modelCatalogue.ts";
|
|
48
50
|
import {
|
|
49
51
|
loadMoaConfig,
|
|
@@ -212,7 +214,7 @@ class MoaModelPickerComponent implements Component {
|
|
|
212
214
|
return undefined;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
private setRole(role: "synthesizer" | "implementer" | "verifier", ref: ModelRef, thinking: ThinkingLevel): void {
|
|
217
|
+
private setRole(role: "synthesizer" | "implementer" | "verifier", ref: ModelRef | undefined, thinking: ThinkingLevel | undefined): void {
|
|
216
218
|
if (role === "synthesizer") { this.synthesizerRef = ref; this.synthesizerThinking = thinking; }
|
|
217
219
|
else if (role === "implementer") { this.implementerRef = ref; this.implementerThinking = thinking; }
|
|
218
220
|
else { this.verifierRef = ref; this.verifierThinking = thinking; }
|
|
@@ -239,11 +241,28 @@ class MoaModelPickerComponent implements Component {
|
|
|
239
241
|
this.tui.requestRender();
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/** Return a slot to `(none)`; the overview cursor stays on its row. */
|
|
245
|
+
private clearSlot(screen: number): void {
|
|
246
|
+
const role = this.nonProposerRole(screen);
|
|
247
|
+
if (role) {
|
|
248
|
+
this.setRole(role, undefined, undefined);
|
|
249
|
+
} else {
|
|
250
|
+
this.proposerRefs[screen - 1] = undefined;
|
|
251
|
+
this.proposerThinking[screen - 1] = undefined;
|
|
252
|
+
}
|
|
253
|
+
this.tui.requestRender();
|
|
254
|
+
}
|
|
255
|
+
|
|
242
256
|
/** Count of proposer slots holding an explicit assignment. */
|
|
243
257
|
private assignedProposerCount(): number {
|
|
244
258
|
return this.proposerRefs.reduce((n, ref) => (ref ? n + 1 : n), 0);
|
|
245
259
|
}
|
|
246
260
|
|
|
261
|
+
private diversityWarning(): string | undefined {
|
|
262
|
+
const assigned = this.proposerRefs.filter((ref): ref is ModelRef => ref !== undefined);
|
|
263
|
+
return proposerDiversityWarning(assigned);
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
/**
|
|
248
267
|
* Ready to start once at least MIN_PROPOSERS proposer slots plus all three
|
|
249
268
|
* required roles hold explicit assignments. Counts assigned slots, not
|
|
@@ -423,6 +442,11 @@ class MoaModelPickerComponent implements Component {
|
|
|
423
442
|
// A disabled Start swallows Enter with no state change.
|
|
424
443
|
return;
|
|
425
444
|
}
|
|
445
|
+
if (matchesKey(data, Key.backspace) || matchesKey(data, Key.delete)) {
|
|
446
|
+
// Load Roster and Start have nothing to clear; every other row is a slot.
|
|
447
|
+
if (this.confirmIndex !== LOAD_ROW && this.confirmIndex !== START_ROW) this.clearSlot(this.confirmIndex);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
426
450
|
if (matchesKey(data, Key.escape)) {
|
|
427
451
|
// Back to the mode chooser; assignments are preserved.
|
|
428
452
|
this.screen = 0;
|
|
@@ -587,14 +611,18 @@ class MoaModelPickerComponent implements Component {
|
|
|
587
611
|
slotRow("Verifier", detail("Verifier", this.verifierRef, this.verifierThinking), VERIFIER_ROW),
|
|
588
612
|
startRow,
|
|
589
613
|
];
|
|
590
|
-
const
|
|
614
|
+
const warning = this.diversityWarning();
|
|
615
|
+
const warningRows = warning
|
|
616
|
+
? wrapWords(warning, bodyWidth - 1).map((line) => row(th.fg("dim", line)))
|
|
617
|
+
: [];
|
|
618
|
+
const reviewRows = [loadRow, row(""), ...slotRows.slice(1, -1), row(""), ...warningRows, startRow];
|
|
591
619
|
const lines = [
|
|
592
620
|
topBorder,
|
|
593
621
|
row(th.fg("accent", "MoA Fusion Pre-flight")),
|
|
594
622
|
sepBorder,
|
|
595
623
|
...reviewRows,
|
|
596
624
|
sepBorder,
|
|
597
|
-
row(th.fg("dim", "\u2191\u2193 navigate \u2022 enter select \u2022 esc back")),
|
|
625
|
+
row(th.fg("dim", "\u2191\u2193 navigate \u2022 enter select \u2022 \u232B clear \u2022 esc back")),
|
|
598
626
|
botBorder,
|
|
599
627
|
];
|
|
600
628
|
if (lines.length <= viewport) return lines;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import type { Message } from "@earendil-works/pi-ai";
|
|
29
29
|
import type { ExtensionContext, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
30
|
-
import { visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
30
|
+
import { visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
31
31
|
import { ACTIVITY_METER_WIDTH, ActivityMeter, rateToLevel, TokRateTracker } from "../activityMeter.ts";
|
|
32
32
|
import { modelRefLabel, shortModelName, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
33
33
|
import { contextPercent, CTX_COL_WIDTH, formatCost, formatElapsed, formatTokens } from "./agentStatus.ts";
|
|
@@ -122,6 +122,14 @@ const AGENT_COL_MIN = 8;
|
|
|
122
122
|
/** Agent width below which a model ref stops being distinguishable from its siblings. */
|
|
123
123
|
const AGENT_COL_READABLE = 24;
|
|
124
124
|
const ACTIVITY_COL_MIN = 10;
|
|
125
|
+
/** Gutter marking a sub-row as belonging to the agent row above it. */
|
|
126
|
+
const SUB_ROW_GUTTER = "↳ ";
|
|
127
|
+
/**
|
|
128
|
+
* Most wrapped lines one failed row may spend on its error detail. A provider
|
|
129
|
+
* error fits well inside this at any usable width; the cap only bites on a
|
|
130
|
+
* pathological message, which must not swallow the whole table.
|
|
131
|
+
*/
|
|
132
|
+
const ERROR_DETAIL_LINES = 8;
|
|
125
133
|
const PREVIEW_LINES = 4;
|
|
126
134
|
const PREVIEW_INDENT = STATUS_COL_WIDTH + 2;
|
|
127
135
|
const PREVIEW_GUTTER = "│ ";
|
|
@@ -297,12 +305,20 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
/**
|
|
300
|
-
* Start the fan-out phase
|
|
308
|
+
* Start the fan-out phase, marking only the slots that fit the process-pool
|
|
309
|
+
* limit as working. The runner promotes queued slots when they acquire one.
|
|
301
310
|
* `thinking[i]` tints proposer `i`'s meter; an absent entry falls to accent.
|
|
302
311
|
*/
|
|
303
|
-
startFanout(refs: ModelRef[], thinking: (ThinkingLevel | undefined)[] = []): void {
|
|
312
|
+
startFanout(refs: ModelRef[], thinking: (ThinkingLevel | undefined)[] = [], maxConcurrency = refs.length): void {
|
|
304
313
|
const startedAt = Date.now();
|
|
305
|
-
|
|
314
|
+
const activeCount = Number.isFinite(maxConcurrency) ? Math.max(1, Math.floor(maxConcurrency)) : refs.length;
|
|
315
|
+
this.statuses = refs.map((ref, index) => ({
|
|
316
|
+
ref,
|
|
317
|
+
phase: "Plan" as const,
|
|
318
|
+
state: index < activeCount ? "working" as const : "queued" as const,
|
|
319
|
+
startedAt: index < activeCount ? startedAt : undefined,
|
|
320
|
+
thinking: thinking[index],
|
|
321
|
+
}));
|
|
306
322
|
this.active = "Plan";
|
|
307
323
|
this.mountTable();
|
|
308
324
|
}
|
|
@@ -341,14 +357,17 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
341
357
|
const wasSettled = s.state === "done" || s.state === "error" || s.state === "cancelled";
|
|
342
358
|
s.state = state;
|
|
343
359
|
s.detail = detail;
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
if (state === "working" && wasSettled) {
|
|
360
|
+
// A settled proposer entering a queued retry/round must clear its preview
|
|
361
|
+
// before it can be promoted to working, so it never shows stale output.
|
|
362
|
+
if ((state === "working" || state === "queued") && wasSettled) {
|
|
347
363
|
s.transcript = undefined;
|
|
348
364
|
s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
|
|
349
365
|
}
|
|
350
366
|
if (!isActive(state)) s.endedAt ??= Date.now();
|
|
351
|
-
else
|
|
367
|
+
else {
|
|
368
|
+
s.startedAt ??= Date.now();
|
|
369
|
+
s.endedAt = undefined;
|
|
370
|
+
}
|
|
352
371
|
}
|
|
353
372
|
|
|
354
373
|
/** Update one proposer's live context, turn, tool-call, and cost readings (by index). */
|
|
@@ -386,6 +405,46 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
386
405
|
s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
|
|
387
406
|
}
|
|
388
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Point a settled proposer row at a replacement model and clear every reading
|
|
410
|
+
* from the abandoned attempt, so the row reads as freshly queued. The elapsed
|
|
411
|
+
* clock is left unstamped: `update(index, "working")` starts it when the slot
|
|
412
|
+
* actually runs. Remounts the table, which the replacement prompt stopped —
|
|
413
|
+
* `update()` alone clears only the transcript, leaving stale turns/tools/cost/
|
|
414
|
+
* context behind, and `stopWidget()` (needed so the select/picker are not
|
|
415
|
+
* fighting the sticky widget) unmounts it, so only `mountTable()` remounts.
|
|
416
|
+
*/
|
|
417
|
+
replaceProposerModel(index: number, ref: ModelRef, thinking?: ThinkingLevel): void {
|
|
418
|
+
const s = this.statuses[index];
|
|
419
|
+
if (!s) return;
|
|
420
|
+
s.ref = ref;
|
|
421
|
+
s.phase = "Plan";
|
|
422
|
+
s.thinking = thinking;
|
|
423
|
+
s.state = "queued";
|
|
424
|
+
s.detail = undefined;
|
|
425
|
+
// Clear every reading from the abandoned attempt, matching the wasSettled
|
|
426
|
+
// branch of update(): the row must read as freshly queued, and the
|
|
427
|
+
// transcriptRevision bump invalidates the preview cache.
|
|
428
|
+
s.contextTokens = undefined;
|
|
429
|
+
s.activity = undefined;
|
|
430
|
+
s.activityHistory = [];
|
|
431
|
+
s.outputTokens = undefined;
|
|
432
|
+
s.outputRevision = undefined;
|
|
433
|
+
s.turns = undefined;
|
|
434
|
+
s.toolCalls = undefined;
|
|
435
|
+
s.costUsd = undefined;
|
|
436
|
+
s.transcript = undefined;
|
|
437
|
+
s.startedAt = undefined;
|
|
438
|
+
s.endedAt = undefined;
|
|
439
|
+
s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
|
|
440
|
+
this.mountTable();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Re-show the table after a prompt stopped it, without changing any row. Idempotent. */
|
|
444
|
+
resumeTable(): void {
|
|
445
|
+
this.mountTable();
|
|
446
|
+
}
|
|
447
|
+
|
|
389
448
|
/**
|
|
390
449
|
* Move a role row into `working`, starting (or keeping) its elapsed clock.
|
|
391
450
|
* Each activation replaces the row's thinking level (with `undefined` when the
|
|
@@ -539,7 +598,7 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
539
598
|
private toRow(s: ProposerStatus, phase: MoaPhase, workingText: string, now: number, firstOfPhase: boolean): ProgressRow {
|
|
540
599
|
const statusText =
|
|
541
600
|
s.state === "working" ? (s.detail ?? workingText)
|
|
542
|
-
: s.state === "cancelling" ? "cancelling
|
|
601
|
+
: s.state === "cancelling" ? "cancelling"
|
|
543
602
|
: s.state === "cancelled" ? "cancelled"
|
|
544
603
|
: s.state === "error" ? s.detail ?? "error"
|
|
545
604
|
: s.state === "queued" ? "queued"
|
|
@@ -768,10 +827,24 @@ export class MoaProgressTableComponent implements Component {
|
|
|
768
827
|
0,
|
|
769
828
|
this.rowBudget() - TABLE_CHROME_ROWS - rows.length - phaseSectionRows(rows) - (band.length > 0 ? PHASE_BAND_ROWS : 0),
|
|
770
829
|
);
|
|
830
|
+
// A failed row's message is the one status a reader has to act on, and it
|
|
831
|
+
// routinely outruns the ACTIVITY cell, so its wrapped detail claims budget
|
|
832
|
+
// ahead of live activity and previews. Failed rows are settled, so they
|
|
833
|
+
// compete for nothing else.
|
|
834
|
+
const subRowWidth = Math.max(0, bodyWidth - STATUS_COL_WIDTH - visibleWidth(SUB_ROW_GUTTER));
|
|
835
|
+
const errorDetails = new Map<number, string[]>();
|
|
836
|
+
let remaining = free;
|
|
837
|
+
for (let i = 0; i < rows.length && remaining > 0; i++) {
|
|
838
|
+
const detail = this.errorDetailLines(rows[i], cols.activity, subRowWidth, Math.min(ERROR_DETAIL_LINES, remaining));
|
|
839
|
+
if (detail.length === 0) continue;
|
|
840
|
+
errorDetails.set(i, detail);
|
|
841
|
+
remaining -= detail.length;
|
|
842
|
+
}
|
|
843
|
+
|
|
771
844
|
const activeIndices = rows.flatMap((r, index) => isActive(r.state) ? [index] : []);
|
|
772
845
|
const activityIndices = activeIndices.filter((index) => rows[index]?.activity);
|
|
773
|
-
const renderedActivityIndices = new Set(activityIndices.slice(0,
|
|
774
|
-
|
|
846
|
+
const renderedActivityIndices = new Set(activityIndices.slice(0, remaining));
|
|
847
|
+
remaining = Math.max(0, remaining - renderedActivityIndices.size);
|
|
775
848
|
|
|
776
849
|
const previewWidth = Math.max(0, leftTableWidth - PREVIEW_INDENT - visibleWidth(PREVIEW_GUTTER));
|
|
777
850
|
const previewIndices = this.view.previewVisible && previewWidth >= PREVIEW_MIN_WIDTH
|
|
@@ -844,6 +917,9 @@ export class MoaProgressTableComponent implements Component {
|
|
|
844
917
|
),
|
|
845
918
|
);
|
|
846
919
|
|
|
920
|
+
const errorDetail = errorDetails.get(i) ?? [];
|
|
921
|
+
for (const [offset, text] of errorDetail.entries()) lines.push(row(this.errorDetailRow(text, offset === 0, subRowWidth)));
|
|
922
|
+
|
|
847
923
|
if (renderedActivityIndices.has(i)) lines.push(row(this.activitySubRow(r, bodyWidth)));
|
|
848
924
|
else if (reservedActivityIndices.has(i)) lines.push(row(""));
|
|
849
925
|
|
|
@@ -881,11 +957,37 @@ export class MoaProgressTableComponent implements Component {
|
|
|
881
957
|
/** Merged-cell tool activity line, spanning everything right of the status icon. */
|
|
882
958
|
private activitySubRow(r: ProgressRow, bodyWidth: number): string {
|
|
883
959
|
const room = Math.max(0, bodyWidth - STATUS_COL_WIDTH);
|
|
884
|
-
const gutter = this.theme.fg("dim",
|
|
960
|
+
const gutter = this.theme.fg("dim", SUB_ROW_GUTTER);
|
|
885
961
|
const activity = highlightActivity(this.theme, r.activity ?? "");
|
|
886
962
|
return `${" ".repeat(STATUS_COL_WIDTH)}${cell(`${gutter}${activity}`, room)}`;
|
|
887
963
|
}
|
|
888
964
|
|
|
965
|
+
/**
|
|
966
|
+
* A failed row's message re-laid across the table body, or nothing when the
|
|
967
|
+
* ACTIVITY cell already shows it whole. Provider failures arrive as one long
|
|
968
|
+
* JSON payload whose cause sits well past the column edge, so the text is
|
|
969
|
+
* wrapped — breaking the payload mid-token when it has no spaces to break on
|
|
970
|
+
* — rather than cut off at a width the reader cannot change.
|
|
971
|
+
*/
|
|
972
|
+
private errorDetailLines(row: ProgressRow, activityWidth: number, width: number, maxLines: number): string[] {
|
|
973
|
+
if (row.state !== "error" || !row.statusText) return [];
|
|
974
|
+
if (width <= 0 || maxLines <= 0) return [];
|
|
975
|
+
if (visibleWidth(row.statusText) <= activityWidth) return [];
|
|
976
|
+
const wrapped = wrapTextWithAnsi(row.statusText, width);
|
|
977
|
+
if (wrapped.length <= maxLines) return wrapped;
|
|
978
|
+
// Mark the cut, so a clipped payload never reads as the whole message.
|
|
979
|
+
const shown = wrapped.slice(0, maxLines);
|
|
980
|
+
shown[maxLines - 1] = fitVisible(`${shown[maxLines - 1]}…`, width, { truncationMark: "…", padToWidth: false });
|
|
981
|
+
return shown;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** One wrapped error line: the gutter carries the first, the rest hang under it. */
|
|
985
|
+
private errorDetailRow(text: string, first: boolean, width: number): string {
|
|
986
|
+
const gutter = first ? this.theme.fg("dim", SUB_ROW_GUTTER) : " ".repeat(visibleWidth(SUB_ROW_GUTTER));
|
|
987
|
+
const detail = fitVisible(text, width, { truncationMark: "…", padToWidth: false });
|
|
988
|
+
return `${" ".repeat(STATUS_COL_WIDTH)}${gutter}${this.theme.fg("error", detail)}`;
|
|
989
|
+
}
|
|
990
|
+
|
|
889
991
|
private topBorder(innerWidth: number, border: (s: string) => string): string {
|
|
890
992
|
const title = ` ${this.view.title} `;
|
|
891
993
|
const head = `${border("══")}${this.theme.fg("accent", title)}`;
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Two kinds of slot are configured here. Planning-subagent slots persist into
|
|
10
10
|
* the frontmatter of the installed agent files; MoA fan-out roles are no
|
|
11
|
-
* longer set per role here — they live in named rosters
|
|
12
|
-
*
|
|
11
|
+
* longer set per role here — they live in named rosters whose completed edits
|
|
12
|
+
* are persisted immediately by the roster manager.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -17,6 +17,10 @@ import { type Component, type TUI } from "@earendil-works/pi-tui";
|
|
|
17
17
|
import {
|
|
18
18
|
defaultThinkingForModel,
|
|
19
19
|
loadMoaConfig,
|
|
20
|
+
MAX_CONCURRENT_AGENTS,
|
|
21
|
+
MAX_VERIFICATION_REPAIRS_LIMIT,
|
|
22
|
+
MIN_CONCURRENT_AGENTS,
|
|
23
|
+
MIN_VERIFICATION_REPAIRS,
|
|
20
24
|
saveMoaConfig,
|
|
21
25
|
type MoaConfig,
|
|
22
26
|
} from "../config/settings.ts";
|
|
@@ -49,8 +53,6 @@ interface SetupSlot {
|
|
|
49
53
|
description: string;
|
|
50
54
|
/** Whether the overview row appends the selected thinking level. */
|
|
51
55
|
showThinkingValue: boolean;
|
|
52
|
-
/** Where a confirmed selection is persisted. */
|
|
53
|
-
target: { kind: "agent"; agentName: string } | { kind: "role" };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const SLOTS: readonly SetupSlot[] = [
|
|
@@ -59,14 +61,12 @@ const SLOTS: readonly SetupSlot[] = [
|
|
|
59
61
|
title: agent.title,
|
|
60
62
|
description: agent.description,
|
|
61
63
|
showThinkingValue: true,
|
|
62
|
-
target: { kind: "agent", agentName: agent.name },
|
|
63
64
|
})),
|
|
64
65
|
{
|
|
65
66
|
menuLabel: "cheap/fast agent",
|
|
66
67
|
title: "Cheap / fast agent — plan-file naming",
|
|
67
68
|
description: "Only summarizes a plan prompt into a short file name. Always called with thinking off and falls back safely if it cannot respond, so pick the cheapest model available.",
|
|
68
69
|
showThinkingValue: false,
|
|
69
|
-
target: { kind: "role" },
|
|
70
70
|
},
|
|
71
71
|
];
|
|
72
72
|
|
|
@@ -77,6 +77,8 @@ interface SetupDraft {
|
|
|
77
77
|
thinkingOverrides: Record<string, ThinkingLevel>;
|
|
78
78
|
autoResolveConflicts: boolean;
|
|
79
79
|
useSummaryName: boolean;
|
|
80
|
+
maxConcurrentAgents: number;
|
|
81
|
+
maxVerificationRepairs: number;
|
|
80
82
|
rosters: PlanRoster[];
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -90,6 +92,8 @@ class MoaSetupComponent implements Component {
|
|
|
90
92
|
private readonly menu: MenuComponent;
|
|
91
93
|
private autoResolveConflicts: boolean;
|
|
92
94
|
private useSummaryName: boolean;
|
|
95
|
+
private maxConcurrentAgents: number;
|
|
96
|
+
private maxVerificationRepairs: number;
|
|
93
97
|
|
|
94
98
|
constructor(
|
|
95
99
|
private readonly tui: TUI,
|
|
@@ -105,6 +109,8 @@ class MoaSetupComponent implements Component {
|
|
|
105
109
|
this.twoPane = new TwoPaneModelThinking(tui, theme, availableRefs, config, currentThinking, ctx);
|
|
106
110
|
this.autoResolveConflicts = draft.autoResolveConflicts;
|
|
107
111
|
this.useSummaryName = draft.useSummaryName;
|
|
112
|
+
this.maxConcurrentAgents = draft.maxConcurrentAgents;
|
|
113
|
+
this.maxVerificationRepairs = draft.maxVerificationRepairs;
|
|
108
114
|
const slotItem = (slotIndex: number): MenuItem => ({
|
|
109
115
|
id: `role-${slotIndex}`,
|
|
110
116
|
label: SLOTS[slotIndex]!.menuLabel,
|
|
@@ -130,6 +136,28 @@ class MoaSetupComponent implements Component {
|
|
|
130
136
|
{ title: "options", items: [
|
|
131
137
|
{ id: "auto-resolve", label: "auto resolve conflicts", value: this.autoResolveConflicts, onChange: (value: boolean) => { this.autoResolveConflicts = value; } },
|
|
132
138
|
{ id: "use-summary-name", label: "summarize plan names", value: this.useSummaryName, onChange: (value: boolean) => { this.useSummaryName = value; } },
|
|
139
|
+
{
|
|
140
|
+
id: "max-concurrent-agents",
|
|
141
|
+
label: "max concurrent agents",
|
|
142
|
+
values: Array.from(
|
|
143
|
+
{ length: MAX_CONCURRENT_AGENTS - MIN_CONCURRENT_AGENTS + 1 },
|
|
144
|
+
(_unused, index) => String(MIN_CONCURRENT_AGENTS + index),
|
|
145
|
+
),
|
|
146
|
+
valueIndex: this.maxConcurrentAgents - MIN_CONCURRENT_AGENTS,
|
|
147
|
+
description: "Child agents running at once in MoA, opinion, debate and plan subagent fan-outs. Keep at 1 for local agents.",
|
|
148
|
+
onChange: (valueIndex: number) => { this.maxConcurrentAgents = MIN_CONCURRENT_AGENTS + valueIndex; },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "max-verification-repairs",
|
|
152
|
+
label: "max verification repairs",
|
|
153
|
+
values: Array.from(
|
|
154
|
+
{ length: MAX_VERIFICATION_REPAIRS_LIMIT - MIN_VERIFICATION_REPAIRS + 1 },
|
|
155
|
+
(_unused, index) => String(MIN_VERIFICATION_REPAIRS + index),
|
|
156
|
+
),
|
|
157
|
+
valueIndex: this.maxVerificationRepairs - MIN_VERIFICATION_REPAIRS,
|
|
158
|
+
description: "Verifier-driven repair rounds after implementation (0 disables automatic repairs).",
|
|
159
|
+
onChange: (valueIndex: number) => { this.maxVerificationRepairs = MIN_VERIFICATION_REPAIRS + valueIndex; },
|
|
160
|
+
},
|
|
133
161
|
] },
|
|
134
162
|
],
|
|
135
163
|
buttons: [
|
|
@@ -168,6 +196,8 @@ class MoaSetupComponent implements Component {
|
|
|
168
196
|
thinkingOverrides: { ...this.draft.thinkingOverrides },
|
|
169
197
|
autoResolveConflicts: this.autoResolveConflicts,
|
|
170
198
|
useSummaryName: this.useSummaryName,
|
|
199
|
+
maxConcurrentAgents: this.maxConcurrentAgents,
|
|
200
|
+
maxVerificationRepairs: this.maxVerificationRepairs,
|
|
171
201
|
rosters: [...this.draft.rosters],
|
|
172
202
|
};
|
|
173
203
|
}
|
|
@@ -299,6 +329,8 @@ export async function showMoaSetup(
|
|
|
299
329
|
thinkingOverrides: {},
|
|
300
330
|
autoResolveConflicts: saved.autoResolveConflicts,
|
|
301
331
|
useSummaryName: saved.useSummaryName,
|
|
332
|
+
maxConcurrentAgents: saved.maxConcurrentAgents,
|
|
333
|
+
maxVerificationRepairs: saved.maxVerificationRepairs,
|
|
302
334
|
rosters: [...saved.rosters],
|
|
303
335
|
};
|
|
304
336
|
|
|
@@ -325,7 +357,17 @@ export async function showMoaSetup(
|
|
|
325
357
|
);
|
|
326
358
|
if (result === undefined) return false;
|
|
327
359
|
if (result.action === "rosters") {
|
|
328
|
-
draft.
|
|
360
|
+
draft.autoResolveConflicts = result.draft.autoResolveConflicts;
|
|
361
|
+
draft.useSummaryName = result.draft.useSummaryName;
|
|
362
|
+
draft.maxConcurrentAgents = result.draft.maxConcurrentAgents;
|
|
363
|
+
draft.maxVerificationRepairs = result.draft.maxVerificationRepairs;
|
|
364
|
+
draft.rosters = await showRosterManager(ctx, draft.rosters, {
|
|
365
|
+
currentThinking,
|
|
366
|
+
onChange: (rosters) => {
|
|
367
|
+
const current = loadMoaConfig();
|
|
368
|
+
saveMoaConfig({ ...current, rosters });
|
|
369
|
+
},
|
|
370
|
+
});
|
|
329
371
|
continue;
|
|
330
372
|
}
|
|
331
373
|
break;
|
|
@@ -360,6 +402,8 @@ export async function showMoaSetup(
|
|
|
360
402
|
agentDefaultsConfigured: true,
|
|
361
403
|
autoResolveConflicts: finalDraft.autoResolveConflicts,
|
|
362
404
|
useSummaryName: finalDraft.useSummaryName,
|
|
405
|
+
maxConcurrentAgents: finalDraft.maxConcurrentAgents,
|
|
406
|
+
maxVerificationRepairs: finalDraft.maxVerificationRepairs,
|
|
363
407
|
cheap: finalDraft.selections[AGENT_SLOT_COUNT],
|
|
364
408
|
rosters: finalDraft.rosters,
|
|
365
409
|
thinkingOverrides: { ...current.thinkingOverrides, ...finalDraft.thinkingOverrides },
|
|
@@ -71,16 +71,15 @@ function formatProposersList(proposers: ModelRef[]): string {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function formatSplitRow(left: string, right: string | undefined, bodyWidth: number): string {
|
|
74
|
-
const leftText = left ?? "";
|
|
75
74
|
if (!right) {
|
|
76
|
-
return padRow(
|
|
75
|
+
return padRow(left, bodyWidth);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
const minLeft = Math.min(visibleWidth(
|
|
78
|
+
const minLeft = Math.min(visibleWidth(left), 18);
|
|
80
79
|
const maxRight = Math.max(1, bodyWidth - minLeft);
|
|
81
80
|
const rightPart = truncateToWidth(right, maxRight, "…", false);
|
|
82
81
|
const rightVis = visibleWidth(rightPart);
|
|
83
|
-
const leftPart = truncateToWidth(
|
|
82
|
+
const leftPart = truncateToWidth(left, Math.max(1, bodyWidth - rightVis), "…", false);
|
|
84
83
|
const gap = bodyWidth - visibleWidth(leftPart) - visibleWidth(rightPart);
|
|
85
84
|
const row = gap > 0 ? leftPart + " ".repeat(gap) + rightPart : leftPart + rightPart;
|
|
86
85
|
return padRow(row, bodyWidth);
|
|
@@ -92,15 +91,23 @@ function formatRightRow(content: string, bodyWidth: number): string {
|
|
|
92
91
|
return padRow(gap > 0 ? " ".repeat(gap) + truncated : truncated, bodyWidth);
|
|
93
92
|
}
|
|
94
93
|
|
|
94
|
+
export interface PlanReviewLabels {
|
|
95
|
+
title?: string;
|
|
96
|
+
approveHint?: string;
|
|
97
|
+
keepHint?: string;
|
|
98
|
+
chatHint?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
export async function showPlanReview(
|
|
96
102
|
ctx: ExtensionContext,
|
|
97
103
|
planMarkdown: string,
|
|
98
104
|
moaInfo?: MfPlanInfo,
|
|
99
105
|
planName?: string,
|
|
100
106
|
allowChat = false,
|
|
107
|
+
labels?: PlanReviewLabels,
|
|
101
108
|
): Promise<PlanReviewDecision> {
|
|
102
109
|
const result = await ctx.ui.custom<PlanReviewDecision>(
|
|
103
|
-
(tui, theme, _keybindings, done) => new PlanReviewOverlay(tui, theme, planMarkdown, moaInfo, ctx.cwd, planName, allowChat, done),
|
|
110
|
+
(tui, theme, _keybindings, done) => new PlanReviewOverlay(tui, theme, planMarkdown, moaInfo, ctx.cwd, planName, allowChat, labels, done),
|
|
104
111
|
{
|
|
105
112
|
overlay: true,
|
|
106
113
|
overlayOptions: {
|
|
@@ -136,6 +143,7 @@ class PlanReviewOverlay implements Component {
|
|
|
136
143
|
private readonly repoCwd: string,
|
|
137
144
|
private readonly planName: string | undefined,
|
|
138
145
|
private readonly allowChat: boolean,
|
|
146
|
+
private readonly labels: PlanReviewLabels | undefined,
|
|
139
147
|
private readonly done: (decision: PlanReviewDecision) => void,
|
|
140
148
|
) {
|
|
141
149
|
this.markdown = new Markdown(stripTerminalSequences(planMarkdown), 0, 0, getMarkdownTheme());
|
|
@@ -234,9 +242,9 @@ class PlanReviewOverlay implements Component {
|
|
|
234
242
|
.join(",");
|
|
235
243
|
const proposalHelp = helpParts ? ` · ${helpParts} proposer plans · \`/0 synthesized` : "";
|
|
236
244
|
const verdictsHelp = this.verdictsMarkdown ? " · v verdicts" : "";
|
|
237
|
-
const chatHelp = this.allowChat ?
|
|
245
|
+
const chatHelp = this.allowChat ? ` · c ${this.labels?.chatHint ?? "chat"}` : "";
|
|
238
246
|
const helpLines = wrapDimLine(
|
|
239
|
-
`↑/↓ or j/k scroll · u/d page${proposalHelp}${verdictsHelp} · e edit${chatHelp} · a/Enter approve · q/Esc keep planning`,
|
|
247
|
+
`↑/↓ or j/k scroll · u/d page${proposalHelp}${verdictsHelp} · e edit${chatHelp} · a/Enter ${this.labels?.approveHint ?? "approve"} · q/Esc ${this.labels?.keepHint ?? "keep planning"}`,
|
|
240
248
|
bodyWidth,
|
|
241
249
|
);
|
|
242
250
|
this.helpLineCount = helpLines.length;
|
|
@@ -261,7 +269,7 @@ class PlanReviewOverlay implements Component {
|
|
|
261
269
|
const planTitle = showingVerdicts
|
|
262
270
|
? "Proposer Verdicts"
|
|
263
271
|
: this.activeProposerIndex === undefined
|
|
264
|
-
? "Proposed Plan"
|
|
272
|
+
? this.labels?.title ?? "Proposed Plan"
|
|
265
273
|
: `Proposer ${this.activeProposerIndex + 1} Plan`;
|
|
266
274
|
const proposerModel = this.activeProposerIndex === undefined
|
|
267
275
|
? undefined
|
package/src/ui/rosterEditor.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A staged CRUD surface over the saved `PlanRoster` list: list → name prompt →
|
|
5
5
|
* slot editor → per-slot model/thinking picker, ported from persona-audit's
|
|
6
|
-
* RosterEditor.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* RosterEditor. Draft slot changes remain in memory until Save roster is
|
|
7
|
+
* chosen; completed roster saves and confirmed deletions are then persisted
|
|
8
|
+
* immediately through the manager's onChange callback.
|
|
9
9
|
*
|
|
10
10
|
* Each roster carries one model + thinking level for every fan-out role —
|
|
11
11
|
* 2–5 proposers plus synthesizer, implementer, and verifier. A roster must be
|
|
@@ -31,8 +31,6 @@ import { ratioViewport } from "./chrome.ts";
|
|
|
31
31
|
import { MenuComponent, showOverlayPrompt, type ActionMenuItem } from "./menu.ts";
|
|
32
32
|
import { showModelThinkingPicker } from "./moaModelPicker.ts";
|
|
33
33
|
|
|
34
|
-
const BACKSPACE = "\x7f";
|
|
35
|
-
|
|
36
34
|
type SlotRole = "synthesizer" | "implementer" | "verifier";
|
|
37
35
|
type SlotTarget = { kind: "proposer"; index: number } | { kind: "role"; role: SlotRole };
|
|
38
36
|
type ListResult = { action: "create" } | { action: "edit"; index: number } | { action: "back" };
|
|
@@ -198,7 +196,7 @@ function showSlotEditor(ctx: ExtensionContext, draft: DraftRoster, focusItemId:
|
|
|
198
196
|
{ title: "roles", items: roleItems },
|
|
199
197
|
],
|
|
200
198
|
onItemKey: (item, data) => {
|
|
201
|
-
if (data
|
|
199
|
+
if (!matchesKey(data, Key.backspace) && !matchesKey(data, Key.delete)) return false;
|
|
202
200
|
const target = parseTargetItemId(item.id);
|
|
203
201
|
if (!target) return true;
|
|
204
202
|
if (target.kind === "proposer") working.proposers[target.index] = undefined;
|
|
@@ -245,7 +243,7 @@ async function editRoster(
|
|
|
245
243
|
if (result.action === "cancel") return undefined;
|
|
246
244
|
if (result.action === "save") return fromDraft(result.draft);
|
|
247
245
|
if (result.action === "delete") {
|
|
248
|
-
if (await ctx.ui.confirm("Delete agent roster?", `Delete ${draft.name}? This
|
|
246
|
+
if (await ctx.ui.confirm("Delete agent roster?", `Delete ${draft.name}? This takes effect immediately.`)) return "delete";
|
|
249
247
|
continue;
|
|
250
248
|
}
|
|
251
249
|
if (result.action === "rename") {
|
|
@@ -272,11 +270,11 @@ async function editRoster(
|
|
|
272
270
|
}
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
/** Manage
|
|
273
|
+
/** Manage roster drafts and publish completed saves immediately through onChange. */
|
|
276
274
|
export async function showRosterManager(
|
|
277
275
|
ctx: ExtensionContext,
|
|
278
276
|
initial: PlanRoster[],
|
|
279
|
-
deps: { currentThinking: ThinkingLevel },
|
|
277
|
+
deps: { currentThinking: ThinkingLevel; onChange?: (rosters: PlanRoster[]) => void },
|
|
280
278
|
): Promise<PlanRoster[]> {
|
|
281
279
|
const rosters = initial.map((roster) => ({ ...roster, proposers: [...roster.proposers] }));
|
|
282
280
|
for (;;) {
|
|
@@ -298,13 +296,21 @@ export async function showRosterManager(
|
|
|
298
296
|
undefined,
|
|
299
297
|
deps.currentThinking,
|
|
300
298
|
);
|
|
301
|
-
if (created && created !== "delete")
|
|
299
|
+
if (created && created !== "delete") {
|
|
300
|
+
rosters.push(created);
|
|
301
|
+
deps.onChange?.(rosters);
|
|
302
|
+
}
|
|
302
303
|
continue;
|
|
303
304
|
}
|
|
304
305
|
const current = rosters[action.index];
|
|
305
306
|
if (!current) continue;
|
|
306
307
|
const edited = await editRoster(ctx, rosters, toDraft(current), action.index, deps.currentThinking);
|
|
307
|
-
if (edited === "delete")
|
|
308
|
-
|
|
308
|
+
if (edited === "delete") {
|
|
309
|
+
rosters.splice(action.index, 1);
|
|
310
|
+
deps.onChange?.(rosters);
|
|
311
|
+
} else if (edited) {
|
|
312
|
+
rosters[action.index] = edited;
|
|
313
|
+
deps.onChange?.(rosters);
|
|
314
|
+
}
|
|
309
315
|
}
|
|
310
316
|
}
|