pi-subagents 0.66.0 → 0.67.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 +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree.ts +42 -12
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +4 -1
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +149 -28
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
package/src/tui/render.ts
CHANGED
|
@@ -45,6 +45,10 @@ import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, format
|
|
|
45
45
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
46
46
|
|
|
47
47
|
interface WorkflowWidgetProjection {
|
|
48
|
+
now: number;
|
|
49
|
+
inlineFleetCovered?: boolean;
|
|
50
|
+
children?: AsyncJobState[];
|
|
51
|
+
materializedKeys?: Set<string>;
|
|
48
52
|
stages: WorkflowGraphNode[];
|
|
49
53
|
steps: AsyncJobStep[];
|
|
50
54
|
stageProgress?: { total: number; current?: number };
|
|
@@ -443,11 +447,14 @@ function workflowStageProgress(job: AsyncJobState, stages = job.mode === "workfl
|
|
|
443
447
|
return { total: stages.length };
|
|
444
448
|
}
|
|
445
449
|
|
|
446
|
-
function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjection {
|
|
450
|
+
function buildWorkflowWidgetProjection(job: AsyncJobState, now = Date.now()): WorkflowWidgetProjection {
|
|
447
451
|
const stages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
448
452
|
const stageProgress = workflowStageProgress(job, stages);
|
|
449
|
-
const
|
|
450
|
-
const
|
|
453
|
+
const clock = job.status === "running" ? now : job.updatedAt;
|
|
454
|
+
const loadedSteps = workflowWidgetSteps(job, stages);
|
|
455
|
+
const steps = job.mode === "workflow" ? loadedSteps.map((step) => step.status === "running" && step.startedAt !== undefined && clock !== undefined
|
|
456
|
+
? { ...step, durationMs: Math.max(0, clock - step.startedAt) } : step) : loadedSteps;
|
|
457
|
+
const projection: WorkflowWidgetProjection = { stages, steps, now };
|
|
451
458
|
if (stageProgress) projection.stageProgress = stageProgress;
|
|
452
459
|
if (stages.length) projection.plannedKeys = new Set(stages.map((node) => node.id));
|
|
453
460
|
if (job.mode === "workflow") {
|
|
@@ -457,19 +464,27 @@ function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjec
|
|
|
457
464
|
hostSteps: job.hostSteps,
|
|
458
465
|
preflight: job.preflight,
|
|
459
466
|
trace: job.workflow?.trace,
|
|
460
|
-
now:
|
|
467
|
+
now: clock,
|
|
461
468
|
});
|
|
462
469
|
if (checklist.total > 0) projection.checklist = checklist;
|
|
463
470
|
}
|
|
464
471
|
return projection;
|
|
465
472
|
}
|
|
466
473
|
|
|
467
|
-
function workflowWidgetProjectionLookup(): WorkflowWidgetProjectionLookup {
|
|
474
|
+
function workflowWidgetProjectionLookup(now = Date.now(), childrenByParent = new Map<string, AsyncJobState[]>()): WorkflowWidgetProjectionLookup {
|
|
468
475
|
const projections = new WeakMap<AsyncJobState, WorkflowWidgetProjection>();
|
|
469
476
|
return (job: AsyncJobState) => {
|
|
470
477
|
const cached = projections.get(job);
|
|
471
478
|
if (cached) return cached;
|
|
472
|
-
const projection = buildWorkflowWidgetProjection(job);
|
|
479
|
+
const projection = buildWorkflowWidgetProjection(job, now);
|
|
480
|
+
const children = childrenByParent.get(job.asyncId);
|
|
481
|
+
if (children?.length) {
|
|
482
|
+
projection.children = children;
|
|
483
|
+
projection.materializedKeys = new Set(children.flatMap((child) => [child.asyncId, ...(child.workflowKey ? [child.workflowKey] : [])]));
|
|
484
|
+
for (const step of projection.steps) {
|
|
485
|
+
if (step.runId && projection.materializedKeys.has(step.runId) && step.workflowKey) projection.materializedKeys.add(step.workflowKey);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
473
488
|
projections.set(job, projection);
|
|
474
489
|
return projection;
|
|
475
490
|
};
|
|
@@ -1051,9 +1066,11 @@ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
|
|
|
1051
1066
|
}
|
|
1052
1067
|
|
|
1053
1068
|
export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
1054
|
-
const projection = buildWorkflowWidgetProjection(job);
|
|
1069
|
+
const projection = buildWorkflowWidgetProjection(job, job.updatedAt ?? 0);
|
|
1055
1070
|
return JSON.stringify({
|
|
1056
1071
|
asyncDir: job.asyncDir,
|
|
1072
|
+
parentWorkflowRunId: job.parentWorkflowRunId,
|
|
1073
|
+
workflowKey: job.workflowKey,
|
|
1057
1074
|
status: job.status,
|
|
1058
1075
|
description: job.mode === "workflow" ? job.description : undefined,
|
|
1059
1076
|
activityState: job.activityState,
|
|
@@ -1501,9 +1518,10 @@ function compactWorkflowStats(job: AsyncJobState, rows: readonly CompactWorkflow
|
|
|
1501
1518
|
const rowToolUses = rows.map((row) => row.toolUses).filter((value): value is number => value !== undefined);
|
|
1502
1519
|
const toolUses = job.toolCount ?? (rowToolUses.length ? rowToolUses.reduce((sum, value) => sum + value, 0) : undefined);
|
|
1503
1520
|
const rowDuration = rows.map((row) => row.durationMs).filter((value): value is number => value !== undefined);
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1521
|
+
const end = job.status === "running" ? projection.now : job.updatedAt;
|
|
1522
|
+
const durationMs = job.status !== "queued" && job.startedAt !== undefined && end !== undefined
|
|
1523
|
+
? Math.max(0, end - job.startedAt)
|
|
1524
|
+
: job.status !== "queued" && rowDuration.length ? Math.max(...rowDuration) : undefined;
|
|
1507
1525
|
return statJoin(theme, [
|
|
1508
1526
|
`id: ${compactWorkflowShortId(job)}`,
|
|
1509
1527
|
...progress,
|
|
@@ -1540,8 +1558,11 @@ function compactWorkflowHeaderLine(job: AsyncJobState, theme: Theme, width?: num
|
|
|
1540
1558
|
function compactWorkflowWidgetBodyLines(job: AsyncJobState, theme: Theme, frame: number | undefined, projection: WorkflowWidgetProjection): string[] {
|
|
1541
1559
|
const rows = compactWorkflowLaneRows(job, projection.checklist);
|
|
1542
1560
|
const lines = [` ${compactWorkflowStats(job, rows, theme, projection)}`];
|
|
1561
|
+
if (projection.inlineFleetCovered) return [...lines, ` ${theme.fg("dim", "Workflow children shown in Fleet roster")}`];
|
|
1543
1562
|
if (rows.length) {
|
|
1544
|
-
for (const row of rows)
|
|
1563
|
+
for (const row of rows) {
|
|
1564
|
+
if (!projection.materializedKeys?.has(row.key)) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
|
|
1565
|
+
}
|
|
1545
1566
|
} else {
|
|
1546
1567
|
lines.push(` ${theme.fg("dim", "◦ waiting for workflow lanes")}`);
|
|
1547
1568
|
}
|
|
@@ -2085,7 +2106,8 @@ function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflo
|
|
|
2085
2106
|
if (projection.checklist && !checklistPrimary) parts.push(formatWorkflowChecklistSummary(projection.checklist));
|
|
2086
2107
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
2087
2108
|
if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
|
|
2088
|
-
|
|
2109
|
+
const end = job.status === "running" && (job.mode === "workflow" || job.parentWorkflowRunId) ? projection.now : job.updatedAt;
|
|
2110
|
+
if (job.status !== "queued" && job.startedAt !== undefined && end !== undefined) parts.push(formatDuration(Math.max(0, end - job.startedAt)));
|
|
2089
2111
|
return statJoin(theme, parts);
|
|
2090
2112
|
}
|
|
2091
2113
|
|
|
@@ -2364,7 +2386,8 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
|
|
|
2364
2386
|
}
|
|
2365
2387
|
|
|
2366
2388
|
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2367
|
-
const
|
|
2389
|
+
const steps = projection.steps.filter((step) => !projection.materializedKeys?.has(step.workflowKey ?? "") && !projection.materializedKeys?.has(step.runId ?? ""));
|
|
2390
|
+
const checklist = widgetChecklistWithoutMaterializedChildren(projection);
|
|
2368
2391
|
if (!expanded && job.mode === "workflow") {
|
|
2369
2392
|
return compactWorkflowWidgetBodyLines(job, theme, frame, projection);
|
|
2370
2393
|
}
|
|
@@ -2372,7 +2395,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2372
2395
|
const lane = projectAsyncLane(job, laneStepForJob(job, steps));
|
|
2373
2396
|
return [
|
|
2374
2397
|
...(expanded ? workflowPreflightLines(job) : []),
|
|
2375
|
-
...workflowChecklistWidgetLines(
|
|
2398
|
+
...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame),
|
|
2376
2399
|
...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
|
|
2377
2400
|
...hostStepWidgetLines(job, theme, " "),
|
|
2378
2401
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
@@ -2382,7 +2405,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2382
2405
|
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
2383
2406
|
const lines: string[] = [
|
|
2384
2407
|
...(expanded ? workflowPreflightLines(job) : []),
|
|
2385
|
-
...workflowChecklistWidgetLines(
|
|
2408
|
+
...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
|
|
2386
2409
|
];
|
|
2387
2410
|
const group = activeParallelWidgetGroup(job);
|
|
2388
2411
|
if (group) {
|
|
@@ -2415,7 +2438,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2415
2438
|
}
|
|
2416
2439
|
|
|
2417
2440
|
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2418
|
-
if (!expanded && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
|
|
2441
|
+
if ((!expanded || projection.inlineFleetCovered) && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
|
|
2419
2442
|
const stats = widgetStats(job, theme, projection, !expanded);
|
|
2420
2443
|
const count = job.mode === "workflow"
|
|
2421
2444
|
? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
|
|
@@ -2723,6 +2746,68 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2723
2746
|
}
|
|
2724
2747
|
|
|
2725
2748
|
const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
|
|
2749
|
+
const inlineWorkflowCoverage = new WeakMap<ExtensionContext["ui"], ReadonlyMap<string, string>>();
|
|
2750
|
+
const asyncWidgetInvalidations = new WeakMap<ExtensionContext["ui"], () => void>();
|
|
2751
|
+
|
|
2752
|
+
/** Include child identity and freshness in the roster's existing snapshot. */
|
|
2753
|
+
export function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string {
|
|
2754
|
+
if (!children.length) return widgetRenderKey(job);
|
|
2755
|
+
return JSON.stringify([widgetRenderKey(job), children.map((child) => [
|
|
2756
|
+
child.asyncId, widgetRenderKey(child, true), child.context,
|
|
2757
|
+
child.steps?.map((step) => [Boolean(step.runner), step.tokens?.window]), Boolean(child.workflowGraph),
|
|
2758
|
+
])]);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
/** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
|
|
2762
|
+
export function setInlineWorkflowCoverage(ui: ExtensionContext["ui"], coverage: ReadonlyMap<string, string>): void {
|
|
2763
|
+
const previous = inlineWorkflowCoverage.get(ui);
|
|
2764
|
+
if ((previous?.size ?? 0) === coverage.size && [...coverage].every(([id, key]) => previous?.get(id) === key)) return;
|
|
2765
|
+
if (coverage.size) inlineWorkflowCoverage.set(ui, coverage);
|
|
2766
|
+
else inlineWorkflowCoverage.delete(ui);
|
|
2767
|
+
asyncWidgetInvalidations.get(ui)?.();
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/** Attach only to loaded workflow parents; orphan jobs remain top-level. */
|
|
2771
|
+
function widgetJobTree(jobs: AsyncJobState[], now: number): { roots: AsyncJobState[]; projectionFor: WorkflowWidgetProjectionLookup } {
|
|
2772
|
+
const parents = new Map(jobs.filter((job) => job.mode === "workflow").map((job) => [job.asyncId, job]));
|
|
2773
|
+
const childrenByParent = new Map<string, AsyncJobState[]>();
|
|
2774
|
+
const roots: AsyncJobState[] = [];
|
|
2775
|
+
for (const job of jobs) {
|
|
2776
|
+
const parent = job.parentWorkflowRunId ? parents.get(job.parentWorkflowRunId) : undefined;
|
|
2777
|
+
// Root-only adaptive summaries cannot advertise live work under a non-running parent.
|
|
2778
|
+
const keepLiveRoot = isProgressiveActiveJob(job) && parent?.status !== "running";
|
|
2779
|
+
if (parent && parent.asyncId !== job.asyncId && !keepLiveRoot) {
|
|
2780
|
+
const children = childrenByParent.get(parent.asyncId) ?? [];
|
|
2781
|
+
children.push(job);
|
|
2782
|
+
childrenByParent.set(parent.asyncId, children);
|
|
2783
|
+
} else roots.push(job);
|
|
2784
|
+
}
|
|
2785
|
+
return { roots, projectionFor: workflowWidgetProjectionLookup(now, childrenByParent) };
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function widgetChecklistWithoutMaterializedChildren(projection: WorkflowWidgetProjection): WorkflowChecklistProjection | undefined {
|
|
2789
|
+
const { checklist, materializedKeys } = projection;
|
|
2790
|
+
if (!checklist || !materializedKeys) return checklist;
|
|
2791
|
+
return { ...checklist, phases: checklist.phases.map((phase) => ({ ...phase, items: phase.items.filter((item) => !materializedKeys.has(item.key)) })) };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function materializedWidgetChildLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame: number | undefined, projectionFor: WorkflowWidgetProjectionLookup): string[] {
|
|
2795
|
+
const { children, inlineFleetCovered } = projectionFor(job);
|
|
2796
|
+
if (inlineFleetCovered || !children?.length) return [];
|
|
2797
|
+
const lines: string[] = [];
|
|
2798
|
+
const shown = orderedWidgetJobs(children).slice(0, MAX_WIDGET_JOBS);
|
|
2799
|
+
for (const [index, child] of shown.entries()) {
|
|
2800
|
+
const projection = projectionFor(child);
|
|
2801
|
+
const last = index === children.length - 1;
|
|
2802
|
+
const identity = child.workflowKey ?? child.asyncId;
|
|
2803
|
+
const stats = widgetStats(child, theme, projection, !expanded);
|
|
2804
|
+
lines.push(` ${last ? "└─" : "├─"} ${theme.bold(identity)} ${widgetStatusGlyph(child, theme, frame)} ${themeBold(theme, widgetJobName(child))}${contextModeBadge(theme, child.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`);
|
|
2805
|
+
for (const detail of foregroundStyleWidgetDetails(child, theme, expanded, Math.max(0, width - 6), frame, projection)) lines.push(` ${last ? " " : "│ "}${detail}`);
|
|
2806
|
+
for (const detail of materializedWidgetChildLines(child, theme, Math.max(0, width - 4), expanded, frame, projectionFor)) lines.push(` ${detail}`);
|
|
2807
|
+
}
|
|
2808
|
+
if (shown.length < children.length) lines.push(` +${children.length - shown.length} more workflow children`);
|
|
2809
|
+
return lines;
|
|
2810
|
+
}
|
|
2726
2811
|
|
|
2727
2812
|
function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
|
|
2728
2813
|
return (tui, theme) => {
|
|
@@ -2731,6 +2816,13 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2731
2816
|
let cachedFrame: number | undefined;
|
|
2732
2817
|
let cachedExpanded: boolean | undefined;
|
|
2733
2818
|
let cachedLines: string[] | undefined;
|
|
2819
|
+
let cachedCoverage = "[]";
|
|
2820
|
+
const invalidate = (): void => {
|
|
2821
|
+
cachedLines = undefined;
|
|
2822
|
+
resetWidgetLayoutSession();
|
|
2823
|
+
tui.requestRender();
|
|
2824
|
+
};
|
|
2825
|
+
asyncWidgetInvalidations.set(ui, invalidate);
|
|
2734
2826
|
const update = (nextJobs: AsyncJobState[]): void => {
|
|
2735
2827
|
jobs = nextJobs;
|
|
2736
2828
|
cachedLines = undefined;
|
|
@@ -2740,23 +2832,47 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2740
2832
|
const component = Object.assign(container, {
|
|
2741
2833
|
dispose(): void {
|
|
2742
2834
|
if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
|
|
2835
|
+
if (asyncWidgetInvalidations.get(ui) === invalidate) asyncWidgetInvalidations.delete(ui);
|
|
2743
2836
|
},
|
|
2744
2837
|
});
|
|
2745
2838
|
container.render = (renderWidth: number): string[] => {
|
|
2746
|
-
const
|
|
2839
|
+
const now = Date.now();
|
|
2840
|
+
const frame = Math.floor(now / WIDGET_ANIMATION_INTERVAL_MS);
|
|
2747
2841
|
const expanded = ui.getToolsExpanded?.() ?? false;
|
|
2748
|
-
|
|
2842
|
+
const coverage = inlineWorkflowCoverage.get(ui);
|
|
2843
|
+
const covered = new Set<string>();
|
|
2844
|
+
if (coverage?.size) {
|
|
2845
|
+
const childrenByParent = new Map<string, AsyncJobState[]>();
|
|
2846
|
+
for (const child of jobs) {
|
|
2847
|
+
if (!child.parentWorkflowRunId) continue;
|
|
2848
|
+
const children = childrenByParent.get(child.parentWorkflowRunId) ?? [];
|
|
2849
|
+
children.push(child);
|
|
2850
|
+
childrenByParent.set(child.parentWorkflowRunId, children);
|
|
2851
|
+
}
|
|
2852
|
+
for (const job of jobs) {
|
|
2853
|
+
const snapshot = coverage.get(job.asyncId);
|
|
2854
|
+
if (snapshot === undefined) continue;
|
|
2855
|
+
const children = childrenByParent.get(job.asyncId) ?? [];
|
|
2856
|
+
if (!children.some((child) => childrenByParent.has(child.asyncId))
|
|
2857
|
+
&& snapshot === inlineWorkflowRenderKey(job, children)) covered.add(job.asyncId);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
const coverageKey = JSON.stringify([...covered]);
|
|
2861
|
+
if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded && cachedCoverage === coverageKey) return cachedLines;
|
|
2862
|
+
if (cachedCoverage !== coverageKey) resetWidgetLayoutSession();
|
|
2863
|
+
cachedCoverage = coverageKey;
|
|
2749
2864
|
const width = Math.max(0, renderWidth - 2);
|
|
2750
|
-
const projectionFor =
|
|
2865
|
+
const { roots, projectionFor } = widgetJobTree(jobs, now);
|
|
2866
|
+
for (const job of roots) projectionFor(job).inlineFleetCovered = covered.has(job.asyncId);
|
|
2751
2867
|
const buildLines = (): string[] => expanded
|
|
2752
|
-
? buildWidgetLinesWithProjection(
|
|
2753
|
-
:
|
|
2754
|
-
? compactSingleWidgetLines(
|
|
2755
|
-
: buildWidgetLinesWithProjection(
|
|
2868
|
+
? buildWidgetLinesWithProjection(roots, theme, width, true, frame, projectionFor)
|
|
2869
|
+
: roots.length === 1 && !projectionFor(roots[0]!).children?.length
|
|
2870
|
+
? compactSingleWidgetLines(roots[0]!, theme, width, frame, projectionFor(roots[0]!))
|
|
2871
|
+
: buildWidgetLinesWithProjection(roots, theme, width, false, frame, projectionFor);
|
|
2756
2872
|
cachedRenderWidth = renderWidth;
|
|
2757
2873
|
cachedFrame = frame;
|
|
2758
2874
|
cachedExpanded = expanded;
|
|
2759
|
-
cachedLines = fitAdaptiveWidgetLines(
|
|
2875
|
+
cachedLines = fitAdaptiveWidgetLines(roots, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
|
|
2760
2876
|
return cachedLines;
|
|
2761
2877
|
};
|
|
2762
2878
|
return component;
|
|
@@ -2765,7 +2881,10 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2765
2881
|
|
|
2766
2882
|
function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): string[] {
|
|
2767
2883
|
if (jobs.length === 0) return [];
|
|
2768
|
-
if (jobs.length === 1) return
|
|
2884
|
+
if (jobs.length === 1) return [
|
|
2885
|
+
...buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!)),
|
|
2886
|
+
...materializedWidgetChildLines(jobs[0]!, theme, width, expanded, frame, projectionFor).map((line) => truncLine(line, width)),
|
|
2887
|
+
];
|
|
2769
2888
|
const running = jobs.filter((job) => job.status === "running");
|
|
2770
2889
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
2771
2890
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
@@ -2782,7 +2901,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2782
2901
|
let slots = MAX_WIDGET_JOBS;
|
|
2783
2902
|
const appendJob = (job: AsyncJobState): void => {
|
|
2784
2903
|
const projection = projectionFor(job);
|
|
2785
|
-
const compactWorkflow = !expanded && job.mode === "workflow";
|
|
2904
|
+
const compactWorkflow = (!expanded || projection.inlineFleetCovered) && job.mode === "workflow";
|
|
2786
2905
|
const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
|
|
2787
2906
|
const details = compactWorkflow
|
|
2788
2907
|
? [
|
|
@@ -2791,7 +2910,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2791
2910
|
: [
|
|
2792
2911
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
2793
2912
|
...widgetLaneDetailLines(job, theme, projection),
|
|
2794
|
-
...workflowChecklistWidgetLines(projection
|
|
2913
|
+
...workflowChecklistWidgetLines(widgetChecklistWithoutMaterializedChildren(projection), theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
|
|
2795
2914
|
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2796
2915
|
];
|
|
2797
2916
|
items.push([
|
|
@@ -2799,6 +2918,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2799
2918
|
? compactWorkflowHeaderLine(job, theme, width)
|
|
2800
2919
|
: `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
2801
2920
|
...details,
|
|
2921
|
+
...materializedWidgetChildLines(job, theme, width, expanded, frame, projectionFor),
|
|
2802
2922
|
]);
|
|
2803
2923
|
};
|
|
2804
2924
|
|
|
@@ -2845,7 +2965,8 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2845
2965
|
}
|
|
2846
2966
|
|
|
2847
2967
|
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
|
|
2848
|
-
|
|
2968
|
+
const { roots, projectionFor } = widgetJobTree(jobs, Date.now());
|
|
2969
|
+
return buildWidgetLinesWithProjection(roots, theme, width, expanded, frame, projectionFor);
|
|
2849
2970
|
}
|
|
2850
2971
|
|
|
2851
2972
|
/**
|
|
@@ -16,6 +16,7 @@ export interface ChildWatchdogConfig {
|
|
|
16
16
|
agentEndTimeoutMs: number;
|
|
17
17
|
maxWarnings: number | null;
|
|
18
18
|
model?: string;
|
|
19
|
+
fallbackModels?: string[];
|
|
19
20
|
thinking?: string | false;
|
|
20
21
|
lsp: WatchdogLspConfig;
|
|
21
22
|
stalemateRepeats: number;
|
|
@@ -47,6 +48,7 @@ export function resolveChildWatchdogConfig(input: {
|
|
|
47
48
|
const enabled = input.config.enabled && (override?.enabled ?? input.config.children.enabled);
|
|
48
49
|
if (!enabled) return undefined;
|
|
49
50
|
const model = override?.model ?? input.config.children.model;
|
|
51
|
+
const fallbackModels = override?.fallbackModels ?? input.config.children.fallbackModels;
|
|
50
52
|
const thinking = override?.thinking ?? input.config.children.thinking;
|
|
51
53
|
const cadence = override?.cadence ?? input.config.children.cadence ?? input.config.cadence;
|
|
52
54
|
return {
|
|
@@ -57,6 +59,7 @@ export function resolveChildWatchdogConfig(input: {
|
|
|
57
59
|
agentEndTimeoutMs: input.config.agentEndTimeoutMs,
|
|
58
60
|
maxWarnings: input.config.maxWarnings,
|
|
59
61
|
...(model ? { model } : {}),
|
|
62
|
+
...(fallbackModels !== undefined ? { fallbackModels: [...fallbackModels] } : {}),
|
|
60
63
|
...(thinking !== undefined ? { thinking } : {}),
|
|
61
64
|
lsp: { ...input.config.lsp },
|
|
62
65
|
stalemateRepeats: input.config.stalemateRepeats,
|
|
@@ -137,6 +140,10 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
137
140
|
const agent = childConfigOptionalString(parsed, "agent");
|
|
138
141
|
const childIndex = childConfigOptionalIndex(parsed, "childIndex");
|
|
139
142
|
const model = childConfigOptionalString(parsed, "model");
|
|
143
|
+
const fallbackModels = parsed.fallbackModels;
|
|
144
|
+
if (fallbackModels !== undefined && (!Array.isArray(fallbackModels) || fallbackModels.some((value) => typeof value !== "string" || !value.trim()))) {
|
|
145
|
+
throw new Error("Invalid child watchdog config: fallbackModels must be an array of non-empty strings.");
|
|
146
|
+
}
|
|
140
147
|
return {
|
|
141
148
|
...(runId ? { runId } : {}),
|
|
142
149
|
...(agent ? { agent } : {}),
|
|
@@ -145,6 +152,7 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
145
152
|
agentEndTimeoutMs: childConfigPositiveInteger(parsed, "agentEndTimeoutMs"),
|
|
146
153
|
maxWarnings: childConfigNullableNonNegativeInteger(parsed, "maxWarnings"),
|
|
147
154
|
...(model ? { model } : {}),
|
|
155
|
+
...(fallbackModels !== undefined ? { fallbackModels: (fallbackModels as string[]).map((value) => value.trim()) } : {}),
|
|
148
156
|
...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
|
|
149
157
|
lsp: childConfigLsp(parsed.lsp),
|
|
150
158
|
stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
|
|
3
|
+
import type { WatchdogEndpointConfig } from "./types.ts";
|
|
3
4
|
import {
|
|
4
5
|
getSupportedThinkingLevels,
|
|
5
6
|
splitKnownThinkingSuffix,
|
|
@@ -43,6 +44,7 @@ export interface ResolvedWatchdogModelInput {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface WatchdogModelRecommendation {
|
|
47
|
+
source: "configured" | "suggested";
|
|
46
48
|
model: string;
|
|
47
49
|
thinking: ThinkingLevel;
|
|
48
50
|
label: string;
|
|
@@ -147,6 +149,7 @@ function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFam
|
|
|
147
149
|
if (!getSupportedThinkingLevels(modelInfo).includes(STRONG_WATCHDOG_THINKING)) continue;
|
|
148
150
|
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "no current session model";
|
|
149
151
|
return {
|
|
152
|
+
source: "suggested",
|
|
150
153
|
model: resolved.model,
|
|
151
154
|
thinking: STRONG_WATCHDOG_THINKING,
|
|
152
155
|
label: preference.label,
|
|
@@ -165,3 +168,20 @@ export function recommendStrongWatchdogModel(ctx: ExtensionContext): WatchdogMod
|
|
|
165
168
|
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "the current session";
|
|
166
169
|
throw new Error(`No authenticated strong complementary watchdog model was found for ${current}. Configure access to Opus 4.8 or GPT 5.5, then run the recommendation again.`);
|
|
167
170
|
}
|
|
171
|
+
|
|
172
|
+
export function recommendWatchdogModel(ctx: ExtensionContext, configured: WatchdogEndpointConfig): WatchdogModelRecommendation {
|
|
173
|
+
if (!configured.model) return recommendStrongWatchdogModel(ctx);
|
|
174
|
+
const resolved = resolveWatchdogModelInput(ctx, configured.model);
|
|
175
|
+
const thinking = resolved.thinking ?? parseWatchdogThinkingInput(configured.thinking) ?? "off";
|
|
176
|
+
return {
|
|
177
|
+
...resolved,
|
|
178
|
+
source: "configured",
|
|
179
|
+
thinking: thinking === false ? "off" : thinking,
|
|
180
|
+
label: "configured watchdog",
|
|
181
|
+
reason: "Keep the configured watchdog model and thinking; no replacement recommendation is needed. This is not a strength or independence assessment.",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatWatchdogRecommendation(recommendation: WatchdogModelRecommendation): string {
|
|
186
|
+
return `${recommendation.source === "configured" ? "Keep configured watchdog" : "Recommended strong watchdog"}: ${recommendation.model}:${recommendation.thinking}`;
|
|
187
|
+
}
|
|
@@ -4,6 +4,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
|
4
4
|
import { Type, type Static } from "typebox";
|
|
5
5
|
import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
|
|
6
6
|
import { agentStreamOptions } from "../shared/agent-stream-options.ts";
|
|
7
|
+
import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
|
|
7
8
|
import { decodeChildWatchdogConfig } from "./child-status.ts";
|
|
8
9
|
import { childResolvedConfig } from "./register-child.ts";
|
|
9
10
|
import { resolveWatchdogReviewModel } from "./review.ts";
|
|
@@ -94,6 +95,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
|
|
|
94
95
|
const config = childResolvedConfig(childConfig);
|
|
95
96
|
const selection = await resolveWatchdogReviewModel(request.ctx, config);
|
|
96
97
|
const auth = selection.auth;
|
|
98
|
+
const sessionId = request.ctx.sessionManager.getSessionId();
|
|
97
99
|
const registeredProvider = (request.ctx.modelRegistry as {
|
|
98
100
|
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|
|
99
101
|
}).getRegisteredProviderConfig?.(selection.model.provider);
|
|
@@ -104,7 +106,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
|
|
|
104
106
|
...streamOptions,
|
|
105
107
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
106
108
|
env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
|
|
107
|
-
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
109
|
+
headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
108
110
|
});
|
|
109
111
|
agent = new Agent({
|
|
110
112
|
initialState: {
|
|
@@ -21,6 +21,7 @@ export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchd
|
|
|
21
21
|
main: {
|
|
22
22
|
enabled: true,
|
|
23
23
|
...(config.model ? { model: config.model } : {}),
|
|
24
|
+
...(config.fallbackModels !== undefined ? { fallbackModels: [...config.fallbackModels] } : {}),
|
|
24
25
|
...(config.thinking !== undefined ? { thinking: config.thinking } : {}),
|
|
25
26
|
},
|
|
26
27
|
stalemateRepeats: config.stalemateRepeats,
|
|
@@ -3,7 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
|
|
4
4
|
import { SLASH_TEXT_RESULT_TYPE } from "../shared/types.ts";
|
|
5
5
|
import { captureWatchdogDiffBaseline, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { formatWatchdogRecommendation, recommendWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
|
|
7
7
|
import { renderWatchdogWarning } from "./render.ts";
|
|
8
8
|
import { createMainWatchdogReview } from "./review.ts";
|
|
9
9
|
import { MainWatchdogRuntime, type WatchdogReviewFunction } from "./runtime.ts";
|
|
@@ -63,11 +63,16 @@ function mainThinkingLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function mainModelLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
|
|
66
|
+
const fallbacks = fallbackLine(snapshot.config.main.fallbackModels);
|
|
66
67
|
if (snapshot.config.main.model) {
|
|
67
68
|
const source = snapshot.sessionModelOverride?.model ? "session override" : "configured";
|
|
68
|
-
return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})`;
|
|
69
|
+
return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})${fallbacks}`;
|
|
69
70
|
}
|
|
70
|
-
return `Main model: ${currentSessionModelLine(ctx)}`;
|
|
71
|
+
return `Main model: ${currentSessionModelLine(ctx)}${fallbacks}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fallbackLine(models: string[] | undefined): string {
|
|
75
|
+
return models === undefined ? "" : ` · fallbacks ${models.length ? models.join(" → ") : "none"}`;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
|
|
@@ -80,19 +85,19 @@ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>):
|
|
|
80
85
|
const bits = [agent];
|
|
81
86
|
if (override.enabled !== undefined) bits.push(boolLabel(override.enabled));
|
|
82
87
|
if (override.model) bits.push(splitKnownThinkingSuffix(override.model).baseModel);
|
|
88
|
+
if (override.fallbackModels !== undefined) bits.push(fallbackLine(override.fallbackModels));
|
|
83
89
|
if (override.thinking !== undefined) bits.push(`thinking ${override.thinking === false ? "off" : override.thinking}`);
|
|
84
90
|
return bits.join(" ");
|
|
85
91
|
}).join("; ")}`
|
|
86
92
|
: "";
|
|
87
|
-
return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
|
|
93
|
+
return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model}${fallbackLine(children.fallbackModels)} · thinking ${thinking}${overrideText}`;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
function recommendationLine(ctx: ExtensionContext): string {
|
|
96
|
+
function recommendationLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
|
|
91
97
|
try {
|
|
92
|
-
|
|
93
|
-
return `Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking} (${recommendation.label}, complementary reviewer)`;
|
|
98
|
+
return formatWatchdogRecommendation(recommendWatchdogModel(ctx, snapshot.config.main));
|
|
94
99
|
} catch (error) {
|
|
95
|
-
return `
|
|
100
|
+
return `Watchdog recommendation unavailable (${messageFromError(error)})`;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -111,7 +116,8 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
111
116
|
"Subagent watchdog",
|
|
112
117
|
`Main: ${boolLabel(snapshot.enabled)}${!snapshot.config.enabled && snapshot.sessionOverride === undefined ? " (default off)" : ""}`,
|
|
113
118
|
`Runtime: ${statusLabel(snapshot.status)}${snapshot.bufferedDeltas > 0 ? ` · buffered deltas ${snapshot.bufferedDeltas}` : ""}`,
|
|
114
|
-
`Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? "repo edits only" : "every non-empty turn delta"}`,
|
|
119
|
+
`Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? snapshot.config.clarification ? "repo edits + bounded main orchestration activity" : "repo edits only" : "every non-empty turn delta"}`,
|
|
120
|
+
`Main clarification: ${snapshot.config.clarification ? "on" : "off"}`,
|
|
115
121
|
`Scope context: ${snapshot.config.scope.enabled ? "on" : "off"}`,
|
|
116
122
|
`Cadence: ${snapshot.config.cadence.everyNTools === null ? "boundary only" : `every ${snapshot.config.cadence.everyNTools} tools + boundary`}`,
|
|
117
123
|
lspLine(snapshot),
|
|
@@ -119,7 +125,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
119
125
|
mainModelLine(snapshot, ctx),
|
|
120
126
|
`Main thinking: ${mainThinkingLine(snapshot, ctx)}`,
|
|
121
127
|
childrenLine(snapshot),
|
|
122
|
-
recommendationLine(ctx),
|
|
128
|
+
recommendationLine(snapshot, ctx),
|
|
123
129
|
`Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
|
|
124
130
|
`Stalemate: ${snapshot.boundaryRepeats}/${snapshot.config.stalemateRepeats}${snapshot.stalemate ? " · stopped" : ""}`,
|
|
125
131
|
`Rules: ${snapshot.config.rules ? `${Object.keys(snapshot.config.rules.roleModels).length} role models · ${snapshot.config.rules.action}` : "none"}`,
|
|
@@ -145,7 +151,7 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
145
151
|
"",
|
|
146
152
|
"Model commands:",
|
|
147
153
|
"- /subagents-watchdog recommend-model",
|
|
148
|
-
"- /subagents-watchdog model recommended",
|
|
154
|
+
"- /subagents-watchdog model recommended (saves to user settings, not project settings)",
|
|
149
155
|
"- /subagents-watchdog model <provider/model[:thinking]>",
|
|
150
156
|
"- /subagents-watchdog model inherit",
|
|
151
157
|
"- /subagents-watchdog session model recommended",
|
|
@@ -171,12 +177,12 @@ function parseThinkingCommand(raw: string): ThinkingLevel | false | null {
|
|
|
171
177
|
return parseWatchdogThinkingInput(value, "/subagents-watchdog thinking") ?? null;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
|
|
180
|
+
function resolveModelCommandValue(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
|
|
175
181
|
const value = raw.trim();
|
|
176
182
|
if (!value) throw new Error("Expected a model, 'recommended', or 'inherit'.");
|
|
177
183
|
if (value === "inherit") return { model: null, thinking: null, description: "current session model and thinking" };
|
|
178
184
|
if (value === "recommended") {
|
|
179
|
-
const recommendation =
|
|
185
|
+
const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
|
|
180
186
|
return {
|
|
181
187
|
model: recommendation.model,
|
|
182
188
|
thinking: recommendation.thinking,
|
|
@@ -191,18 +197,18 @@ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): {
|
|
|
191
197
|
};
|
|
192
198
|
}
|
|
193
199
|
|
|
194
|
-
function buildRecommendationText(ctx: ExtensionCommandContext): string {
|
|
195
|
-
const recommendation =
|
|
200
|
+
function buildRecommendationText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext): string {
|
|
201
|
+
const recommendation = recommendWatchdogModel(ctx, runtime.getSnapshot(ctx.cwd).config.main);
|
|
196
202
|
return [
|
|
197
203
|
"Subagent watchdog recommended model",
|
|
198
204
|
`Current session: ${currentSessionModelLine(ctx as ExtensionContext)}`,
|
|
199
|
-
`Recommended: ${recommendation.model}:${recommendation.thinking}`,
|
|
205
|
+
recommendation.source === "configured" ? formatWatchdogRecommendation(recommendation) : `Recommended: ${recommendation.model}:${recommendation.thinking}`,
|
|
200
206
|
`Reason: ${recommendation.reason}`,
|
|
201
207
|
"",
|
|
202
208
|
"Apply for this session:",
|
|
203
209
|
"/subagents-watchdog session model recommended",
|
|
204
210
|
"",
|
|
205
|
-
"Save as your user default:",
|
|
211
|
+
"Save as your user default (affects other projects without overrides; does not change project settings):",
|
|
206
212
|
"/subagents-watchdog model recommended",
|
|
207
213
|
].join("\n");
|
|
208
214
|
}
|
|
@@ -221,12 +227,7 @@ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandConte
|
|
|
221
227
|
}
|
|
222
228
|
lines.push(`Main thinking: ${mainThinkingLine(snapshot, ctx as ExtensionContext)}`);
|
|
223
229
|
lines.push(lspLine(snapshot));
|
|
224
|
-
|
|
225
|
-
const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
|
|
226
|
-
lines.push(`Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking}`);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
lines.push(`Recommended strong watchdog: unavailable (${messageFromError(error)})`);
|
|
229
|
-
}
|
|
230
|
+
lines.push(recommendationLine(snapshot, ctx));
|
|
230
231
|
return lines.join("\n");
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -258,7 +259,7 @@ async function handleWatchdogCommand(
|
|
|
258
259
|
}
|
|
259
260
|
if (input === "recommend-model") {
|
|
260
261
|
try {
|
|
261
|
-
sendSlashText(pi, buildRecommendationText(ctx));
|
|
262
|
+
sendSlashText(pi, buildRecommendationText(runtime, ctx));
|
|
262
263
|
} catch (error) {
|
|
263
264
|
sendSlashText(pi, `Subagent watchdog recommended model\n\n${messageFromError(error)}`);
|
|
264
265
|
}
|
|
@@ -301,7 +302,7 @@ async function handleWatchdogCommand(
|
|
|
301
302
|
if (input.startsWith("session model ")) {
|
|
302
303
|
const rawModel = input.slice("session model ".length);
|
|
303
304
|
try {
|
|
304
|
-
const value = resolveModelCommandValue(ctx, rawModel);
|
|
305
|
+
const value = resolveModelCommandValue(runtime, ctx, rawModel);
|
|
305
306
|
const snapshot = value.model === null
|
|
306
307
|
? runtime.clearSessionModel(ctx.cwd)
|
|
307
308
|
: runtime.setSessionModel({ model: value.model, thinking: value.thinking ?? null }, ctx.cwd);
|
|
@@ -319,7 +320,7 @@ async function handleWatchdogCommand(
|
|
|
319
320
|
if (input.startsWith("model ")) {
|
|
320
321
|
const rawModel = input.slice("model ".length);
|
|
321
322
|
try {
|
|
322
|
-
const value = resolveModelCommandValue(ctx, rawModel);
|
|
323
|
+
const value = resolveModelCommandValue(runtime, ctx, rawModel);
|
|
323
324
|
const settingsPath = writeWatchdogModelSettings({
|
|
324
325
|
scope: "user",
|
|
325
326
|
target: { kind: "main" },
|
|
@@ -329,7 +330,7 @@ async function handleWatchdogCommand(
|
|
|
329
330
|
runtime.refreshConfig(ctx.cwd);
|
|
330
331
|
const snapshot = runtime.getSnapshot(ctx.cwd);
|
|
331
332
|
sendSlashText(pi, [
|
|
332
|
-
`Subagent watchdog model saved: ${value.description}.`,
|
|
333
|
+
`Subagent watchdog model saved to user settings: ${value.description}. Project and session overrides still take precedence.`,
|
|
333
334
|
`Updated: ${settingsPath}`,
|
|
334
335
|
`Main now: ${boolLabel(snapshot.enabled)}`,
|
|
335
336
|
value.model === null ? "The watchdog now inherits the current session model and thinking." : "Run /subagents-watchdog on if the watchdog is still off.",
|
|
@@ -387,6 +388,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
387
388
|
reviewDescription: options.review ? "injected seam" : "real model review",
|
|
388
389
|
reviewChangesOnly: true,
|
|
389
390
|
displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
|
|
391
|
+
displayClarification: (content) => pi.sendMessage({ customType: "subagent_watchdog_clarification", content, display: true }, { deliverAs: "steer", triggerTurn: true }),
|
|
390
392
|
});
|
|
391
393
|
|
|
392
394
|
pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
|
|
@@ -421,6 +423,8 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
421
423
|
rememberContext(ctx);
|
|
422
424
|
runtime.handleTurnEnd(event, ctx);
|
|
423
425
|
});
|
|
426
|
+
pi.on("input", (event) => { if (event.source !== "extension") runtime.handleUserInput(); });
|
|
427
|
+
pi.on("model_select", () => runtime.handleModelChange());
|
|
424
428
|
pi.on("tool_result", (_event, ctx) => {
|
|
425
429
|
rememberContext(ctx);
|
|
426
430
|
runtime.handleToolResult(ctx);
|