@ferris1225/pi-subagents 4.1.1 → 4.1.3
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/README.md +461 -381
- package/agents/cleaner.md +16 -6
- package/agents/documenter.md +46 -0
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +8 -4
- package/agents/worker.md +9 -4
- package/package.json +55 -55
- package/src/agents.ts +53 -0
- package/src/announcements.ts +18 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +43 -13
- package/src/dispatch.ts +233 -303
- package/src/fixloop.ts +259 -62
- package/src/index.ts +3 -3
- package/src/models.ts +189 -189
- package/src/monitor.ts +101 -22
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +29 -10
- package/src/runtime.ts +13 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +162 -130
- package/src/spawn.ts +53 -13
- package/src/thread-lifecycle.ts +240 -54
- package/src/tools.ts +65 -37
- package/src/widget.ts +68 -22
- package/src/worktree.ts +27 -4
package/src/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread controls and lookup tools around the subagent runtime:
|
|
3
|
-
* subagent_control (steer/retarget/park/resume), subagent_wait (in-turn
|
|
4
|
-
* lookup), subagent_status, and destructive subagent_stop.
|
|
3
|
+
* subagent_control (steer/retarget/park/resume/fork), subagent_wait (in-turn
|
|
4
|
+
* result lookup), subagent_status, and destructive subagent_stop.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -12,8 +12,7 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "./config.ts";
|
|
|
12
12
|
import { formatCompletionBlock, formatUsage, matchRunIds } from "./format.ts";
|
|
13
13
|
import { emptyUsage } from "./rpc-run.ts";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
formatUsageCompact,
|
|
15
|
+
formatTaskSummary,
|
|
17
16
|
isRunActiveStatus,
|
|
18
17
|
monitor,
|
|
19
18
|
runLabel,
|
|
@@ -51,7 +50,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
51
50
|
Type.String({ description: "Instruction queued by steer after the current child tool batch." }),
|
|
52
51
|
),
|
|
53
52
|
objective: Type.Optional(
|
|
54
|
-
Type.String({ description: "Replacement objective for retarget
|
|
53
|
+
Type.String({ description: "Replacement objective for retarget; optional appended objective for resume/fork. Omit on resume/fork to continue the current retained objective." }),
|
|
55
54
|
),
|
|
56
55
|
});
|
|
57
56
|
|
|
@@ -60,17 +59,18 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
60
59
|
label: "Subagent Control",
|
|
61
60
|
description: [
|
|
62
61
|
"Control an existing sub-agent thread by stable run id.",
|
|
63
|
-
"steer queues an instruction after the current child
|
|
64
|
-
"retarget
|
|
62
|
+
"steer queues an instruction after the current tool batch while the top-level RPC child is active.",
|
|
63
|
+
"retarget replaces the objective in that same active top-level child.",
|
|
64
|
+
"Managed downstream documenter/reviewer/fix stages are controlled by the parent queue rather than its settled RPC control: use park or stop there, then resume with an objective to redirect retained context.",
|
|
65
65
|
"park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
|
|
66
|
-
"resume restarts a parked, completed, or failed retained thread with the same run id; objective
|
|
67
|
-
"fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; objective
|
|
66
|
+
"resume restarts a parked, completed, or failed retained thread with the same run id and cumulative active time; omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed current goal.",
|
|
67
|
+
"fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; omit objective to continue the current goal, or provide one to append a new branch goal.",
|
|
68
68
|
].join(" "),
|
|
69
|
-
promptSnippet: "Control a subagent thread: steer
|
|
69
|
+
promptSnippet: "Control a subagent thread: steer/retarget an active top-level child; park/stop a managed downstream stage; resume or fork retained context.",
|
|
70
70
|
promptGuidelines: [
|
|
71
|
-
"Use subagent_control steer to refine active
|
|
72
|
-
"Use subagent_control retarget
|
|
73
|
-
"Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
|
|
71
|
+
"Use subagent_control steer to refine an active top-level RPC child without restarting it; the instruction is delivered after its current tool batch.",
|
|
72
|
+
"Use subagent_control retarget only while that top-level child is active. During a managed downstream stage, park it and resume with a replacement objective instead.",
|
|
73
|
+
"Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later. Resume without objective keeps the current goal; resume with objective appends that goal to retained context.",
|
|
74
74
|
"Use subagent_control fork only on a parked or settled retained thread; isolated work must settle and integrate before it can fork. Fork creates a new run id while leaving the source untouched.",
|
|
75
75
|
"Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
|
|
76
76
|
],
|
|
@@ -109,13 +109,15 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
109
109
|
thread.task = objective;
|
|
110
110
|
thread.control.retargetPending(objective);
|
|
111
111
|
monitor.setTask(thread.id, objective);
|
|
112
|
-
|
|
112
|
+
monitor.setContinuationKind(thread.id, "retarget");
|
|
113
|
+
return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the replacement objective; no child was spawned by this control action.` }], details: {} };
|
|
113
114
|
}
|
|
114
115
|
if (!(["starting", "running", "steering", "interrupting", "retrying"] as const).includes(phase as any)) {
|
|
115
116
|
return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; use resume with objective to restart retained context.` }], details: {} };
|
|
116
117
|
}
|
|
117
118
|
thread.task = objective;
|
|
118
119
|
monitor.setTask(thread.id, objective);
|
|
120
|
+
monitor.setContinuationKind(thread.id, "retarget");
|
|
119
121
|
await thread.control.retarget(objective);
|
|
120
122
|
return { content: [{ type: "text", text: `Retargeted run #${thread.id} in the same session; the aborted objective will not be delivered as a completion.` }], details: {} };
|
|
121
123
|
}
|
|
@@ -145,11 +147,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
145
147
|
if (params.objective !== undefined && !objective) {
|
|
146
148
|
return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
|
|
147
149
|
}
|
|
150
|
+
const hadRetainedSession = Boolean(thread.sessionId && thread.sessionDir);
|
|
148
151
|
const pending = await thread.resume(objective, ctx);
|
|
149
152
|
if (pending.exitCode !== -1) {
|
|
150
153
|
return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
|
|
151
154
|
}
|
|
152
|
-
|
|
155
|
+
const currentObjective = formatTaskSummary(objective ?? thread.task, 80, false);
|
|
156
|
+
const mode = objective
|
|
157
|
+
? `appended objective: ${currentObjective}`
|
|
158
|
+
: `continuing current objective: ${currentObjective}`;
|
|
159
|
+
const context = hadRetainedSession
|
|
160
|
+
? "the same retained session and prior context are preserved"
|
|
161
|
+
: "no prior child session existed, so only the logical run and objective are continued";
|
|
162
|
+
return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
|
|
153
163
|
}
|
|
154
164
|
case "fork": {
|
|
155
165
|
const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
|
|
@@ -163,7 +173,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
163
173
|
return {
|
|
164
174
|
content: [{
|
|
165
175
|
type: "text",
|
|
166
|
-
text: `Forked run #${thread.id} into new run #${pending.runId}${objective ?
|
|
176
|
+
text: `Forked run #${thread.id} into new run #${pending.runId}; ${objective ? `appended branch objective: ${formatTaskSummary(objective, 80, false)}` : `continuing current objective: ${formatTaskSummary(thread.task, 80, false)}`}. Retained context is copied, the source is unchanged, and child completion will arrive automatically.`,
|
|
167
177
|
}],
|
|
168
178
|
details: { sourceRunId: thread.id, childRunId: pending.runId, result: pending },
|
|
169
179
|
terminate: true,
|
|
@@ -351,10 +361,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
351
361
|
});
|
|
352
362
|
|
|
353
363
|
// Status overview: what is running right now and what finished this session,
|
|
354
|
-
// with per-run details (id,
|
|
355
|
-
// main agent can decide whether to wait, stop, or re-dispatch. Learned from
|
|
356
|
-
// nicobailon/pi-subagents ({action:"status"} + status files): inspect before
|
|
357
|
-
// you act, and report run ids when handing off.
|
|
364
|
+
// with per-run details (id, role, model, usage, elapsed, activity).
|
|
358
365
|
const SubagentStatusParams = Type.Object({
|
|
359
366
|
id: Type.Optional(
|
|
360
367
|
Type.String({
|
|
@@ -367,7 +374,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
367
374
|
name: "subagent_status",
|
|
368
375
|
label: "Subagent Status",
|
|
369
376
|
description: [
|
|
370
|
-
"List active background sub-agent runs (id,
|
|
377
|
+
"List active background sub-agent runs (id, role, model, thinking, usage, elapsed, current activity) and recently finished results.",
|
|
371
378
|
"Pass id to read the full result of a finished run; pass no id for the overview.",
|
|
372
379
|
"Use it to decide whether to subagent_wait, subagent_stop, or re-dispatch — never to poll: results arrive by themselves.",
|
|
373
380
|
].join(" "),
|
|
@@ -409,18 +416,32 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
409
416
|
if (active) {
|
|
410
417
|
const parked = active.status === "parked";
|
|
411
418
|
const activeThread = runtime.threads.get(active.id);
|
|
419
|
+
const managedDownstream =
|
|
420
|
+
activeThread?.state === "running" && activeThread.control.getPhase() === "settled";
|
|
421
|
+
const activeChild = runs.find((run) =>
|
|
422
|
+
run.parentRunId === active.id && isRunActiveStatus(run.status)
|
|
423
|
+
);
|
|
424
|
+
const owner = active.managedWorkflow ? `${active.agent} workflow` : active.agent;
|
|
425
|
+
const retainedStage = active.managedWorkflow && activeThread?.agentName !== active.agent
|
|
426
|
+
? activeThread?.agentName
|
|
427
|
+
: undefined;
|
|
412
428
|
const metadata = [
|
|
413
429
|
activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
|
|
414
430
|
activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
|
|
415
431
|
(activeThread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${activeThread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
416
432
|
].filter(Boolean).join(" · ");
|
|
433
|
+
const stageStatus = activeChild
|
|
434
|
+
? monitor.summarize(activeChild)
|
|
435
|
+
: active.activity ?? statusLabel(active.status);
|
|
417
436
|
return {
|
|
418
437
|
content: [
|
|
419
438
|
{
|
|
420
439
|
type: "text",
|
|
421
440
|
text: parked
|
|
422
|
-
? `Run #${active.id} ${
|
|
423
|
-
:
|
|
441
|
+
? `Run #${active.id} ${owner} is parked with retained${retainedStage ? ` ${retainedStage} stage` : ""} context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
|
|
442
|
+
: managedDownstream
|
|
443
|
+
? `Run #${active.id} ${owner} is in a managed downstream stage (${stageStatus}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait for its result, subagent_control park to checkpoint it, or subagent_stop to cancel it. Steer/retarget are unavailable until you park and resume the retained stage.`
|
|
444
|
+
: `Run #${active.id} ${owner} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
|
|
424
445
|
},
|
|
425
446
|
],
|
|
426
447
|
details: {},
|
|
@@ -429,36 +450,34 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
429
450
|
return { content: [{ type: "text", text: `No subagent run matches "${requested}".` }], details: {} };
|
|
430
451
|
}
|
|
431
452
|
|
|
432
|
-
const now = Date.now();
|
|
433
453
|
const activeRuns = monitor.getRuns().filter(
|
|
434
454
|
(run) => isRunActiveStatus(run.status),
|
|
435
455
|
);
|
|
436
456
|
const activeLines = activeRuns.map((run) => {
|
|
437
457
|
const thread = runtime.threads.get(run.id);
|
|
438
|
-
const model = run.modelFallbackFrom
|
|
439
|
-
? `${run.model ?? "?"} (main after ${run.modelFallbackFrom} failed)`
|
|
440
|
-
: (run.model ?? "?");
|
|
441
458
|
const parts = [
|
|
442
|
-
`#${run.id} ${run
|
|
459
|
+
`#${run.id} ${monitor.summarize(run)}`,
|
|
443
460
|
run.label,
|
|
444
|
-
model,
|
|
445
|
-
formatUsageCompact(run.usage),
|
|
446
|
-
formatElapsed(run, now),
|
|
447
|
-
thread?.isolation === "worktree" ? `worktree ${run.integrationStatus ?? thread.worktree?.state ?? "active"}` : undefined,
|
|
448
461
|
thread?.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
449
462
|
(thread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${thread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
463
|
+
run.activity ?? statusLabel(run.status),
|
|
450
464
|
].filter(Boolean);
|
|
451
|
-
return `- ${parts.join(" · ")}
|
|
465
|
+
return `- ${parts.join(" · ")}`;
|
|
452
466
|
});
|
|
453
467
|
const parkedThreads = [...runtime.threads.values()].filter((thread) => thread.state === "parked");
|
|
454
468
|
const parkedLines = parkedThreads.map((thread) => {
|
|
469
|
+
const run = monitor.findRun(thread.id);
|
|
470
|
+
const owner = run?.managedWorkflow ? `${run.agent} workflow` : run?.agent ?? thread.agentName;
|
|
471
|
+
const retainedStage = run?.managedWorkflow && thread.agentName !== run.agent
|
|
472
|
+
? ` · retained stage ${thread.agentName}`
|
|
473
|
+
: "";
|
|
455
474
|
const relations = [
|
|
456
475
|
thread.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
457
476
|
thread.forkChildRunIds.length > 0 ? `forks ${thread.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
458
477
|
].filter(Boolean);
|
|
459
478
|
const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
460
479
|
const isolation = thread.isolation === "worktree" ? ` · worktree ${thread.worktree?.state ?? "active"}` : "";
|
|
461
|
-
return `- #${thread.id} ${
|
|
480
|
+
return `- #${thread.id} ${owner} · ${run?.label ?? runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${retainedStage}${isolation}${relation}`;
|
|
462
481
|
});
|
|
463
482
|
const completed = [...runtime.settledRuns.entries()].slice(-5);
|
|
464
483
|
const completedLines = completed.map(([id, result]) => {
|
|
@@ -617,9 +636,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
617
636
|
});
|
|
618
637
|
}
|
|
619
638
|
|
|
639
|
+
// Interrupt every claimed generation before awaiting any one of them.
|
|
640
|
+
// An isolated stop may need the repository lane for final integration;
|
|
641
|
+
// cancelling all holders first prevents stop-all from waiting behind a
|
|
642
|
+
// later shared workflow that this same operation has not interrupted yet.
|
|
643
|
+
const interruptionPromises = claimed.map(({ thread, stopMessage, controller }) => {
|
|
644
|
+
const stopping = thread.control.stop(stopMessage).catch(() => undefined);
|
|
645
|
+
runtime.backgroundQueue.cancel(controller);
|
|
646
|
+
return stopping;
|
|
647
|
+
});
|
|
648
|
+
|
|
620
649
|
const stopped: string[] = [];
|
|
621
650
|
const retainedIntegration: string[] = [];
|
|
622
|
-
for (const claim of claimed) {
|
|
651
|
+
for (const [claimIndex, claim] of claimed.entries()) {
|
|
623
652
|
const {
|
|
624
653
|
runId,
|
|
625
654
|
thread,
|
|
@@ -634,8 +663,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
634
663
|
stopVersion,
|
|
635
664
|
stopMessage,
|
|
636
665
|
} = claim;
|
|
637
|
-
await
|
|
638
|
-
runtime.backgroundQueue.cancel(controller);
|
|
666
|
+
await interruptionPromises[claimIndex];
|
|
639
667
|
await completion;
|
|
640
668
|
if (runtime.runControllers.get(runId) === controller) runtime.runControllers.delete(runId);
|
|
641
669
|
if (thread.queueController === controller) thread.queueController = undefined;
|
package/src/widget.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import {
|
|
6
|
+
continuationLabel,
|
|
6
7
|
formatElapsed,
|
|
7
8
|
formatTaskSummary,
|
|
8
9
|
isRunActiveStatus,
|
|
@@ -25,10 +26,22 @@ function compactLine(left: string, right: string, width: number): string {
|
|
|
25
26
|
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** Keep continuation semantics intact while independently compacting the task. */
|
|
30
|
+
function formatContinuationTask(label: string, task: string, width: number): string {
|
|
31
|
+
if (width <= 0) return "";
|
|
32
|
+
const labelWidth = visibleWidth(label);
|
|
33
|
+
if (labelWidth >= width) return truncateToWidth(label, width, "");
|
|
34
|
+
const separator = " · ";
|
|
35
|
+
const taskWidth = width - labelWidth - visibleWidth(separator);
|
|
36
|
+
if (taskWidth <= 0) return label;
|
|
37
|
+
const summary = formatTaskSummary(task, taskWidth);
|
|
38
|
+
return summary ? `${label}${separator}${summary}` : label;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/** One compact primary line per genuinely active run, plus an optional indented
|
|
29
|
-
* activity line. The primary line reserves
|
|
30
|
-
* width before truncating the task. Settled and parked threads never
|
|
31
|
-
* elapsed time cannot keep ticking beside a terminal status. */
|
|
42
|
+
* activity line. The primary line reserves stage model/thinking when present and
|
|
43
|
+
* elapsed width before truncating the task. Settled and parked threads never
|
|
44
|
+
* appear, so elapsed time cannot keep ticking beside a terminal status. */
|
|
32
45
|
function runPrimaryLine(
|
|
33
46
|
run: RunView,
|
|
34
47
|
theme: Theme,
|
|
@@ -38,22 +51,31 @@ function runPrimaryLine(
|
|
|
38
51
|
): string {
|
|
39
52
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
40
53
|
const icon = statusIcon(run.status, theme);
|
|
41
|
-
const
|
|
54
|
+
const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
|
|
55
|
+
const name = theme.fg("accent", theme.bold(displayName));
|
|
42
56
|
const identity = `${prefix}${icon} ${name}`;
|
|
43
57
|
const elapsed = formatElapsed(run, now);
|
|
44
58
|
// Render only the resolved model id plus thinking level. Provider auth and
|
|
45
59
|
// other configuration never enter monitor state or this line.
|
|
46
60
|
const modelId = run.model?.split("/").at(-1);
|
|
47
|
-
const modelSource =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const modelSource = run.managedWorkflow
|
|
62
|
+
? ""
|
|
63
|
+
: formatTaskSummary(
|
|
64
|
+
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
65
|
+
64,
|
|
66
|
+
false,
|
|
67
|
+
);
|
|
52
68
|
// A chain child shows its role in the chain plus a task-derived label; the
|
|
53
69
|
// templated fix brief itself would only repeat the parent review's content.
|
|
70
|
+
const continuation = run.parentRunId === undefined
|
|
71
|
+
? continuationLabel(run.continuationKind, run.forkedFromRunId)
|
|
72
|
+
: undefined;
|
|
54
73
|
const taskSource = run.parentRunId !== undefined
|
|
55
74
|
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
|
|
56
75
|
: formatTaskSummary(run.task, 64);
|
|
76
|
+
const taskDesiredSource = [continuation, taskSource]
|
|
77
|
+
.filter((part): part is string => Boolean(part))
|
|
78
|
+
.join(" · ");
|
|
57
79
|
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
58
80
|
const contentWidth = Math.max(
|
|
59
81
|
0,
|
|
@@ -64,15 +86,40 @@ function runPrimaryLine(
|
|
|
64
86
|
);
|
|
65
87
|
const modelDesired = visibleWidth(modelSource);
|
|
66
88
|
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
67
|
-
let modelWidth =
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
let modelWidth = 0;
|
|
90
|
+
if (modelSource) {
|
|
91
|
+
if (continuation) {
|
|
92
|
+
const continuationWidth = visibleWidth(continuation);
|
|
93
|
+
// Preserve the full semantic label and the usual eight-column task tail
|
|
94
|
+
// before giving the remaining space to model/thinking.
|
|
95
|
+
const taskFloor = Math.min(
|
|
96
|
+
visibleWidth(taskDesiredSource),
|
|
97
|
+
continuationWidth +
|
|
98
|
+
(taskSource ? visibleWidth(" · ") + Math.min(8, visibleWidth(taskSource)) : 0),
|
|
99
|
+
contentWidth,
|
|
100
|
+
);
|
|
101
|
+
const effectiveModelFloor = Math.min(
|
|
102
|
+
modelFloor,
|
|
103
|
+
Math.max(0, contentWidth - continuationWidth),
|
|
104
|
+
);
|
|
105
|
+
modelWidth = Math.min(
|
|
106
|
+
modelDesired,
|
|
107
|
+
Math.max(effectiveModelFloor, contentWidth - taskFloor),
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
modelWidth = Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
70
113
|
let taskWidth = contentWidth - modelWidth;
|
|
71
|
-
if (visibleWidth(
|
|
72
|
-
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(
|
|
114
|
+
if (visibleWidth(taskDesiredSource) < taskWidth) {
|
|
115
|
+
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskDesiredSource));
|
|
73
116
|
taskWidth = contentWidth - modelWidth;
|
|
74
117
|
}
|
|
75
|
-
const task = taskWidth
|
|
118
|
+
const task = taskWidth <= 0
|
|
119
|
+
? ""
|
|
120
|
+
: continuation
|
|
121
|
+
? formatContinuationTask(continuation, taskSource, taskWidth)
|
|
122
|
+
: formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined);
|
|
76
123
|
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
77
124
|
const primaryLeft = [
|
|
78
125
|
identity,
|
|
@@ -93,10 +140,9 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
|
|
|
93
140
|
return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
|
|
94
141
|
}
|
|
95
142
|
|
|
96
|
-
/** Render active runs as a tree: main-agent dispatches are roots
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* available through subagent_status when a thread must be controlled. */
|
|
143
|
+
/** Render active runs as a tree: main-agent dispatches are roots and managed
|
|
144
|
+
* documenter/reviewer/fix steps nest under the stable parent row. Fork labels
|
|
145
|
+
* include their source id; other control ids remain available through status. */
|
|
100
146
|
export function formatActiveRunLines(
|
|
101
147
|
runs: readonly RunView[],
|
|
102
148
|
theme: Theme,
|
|
@@ -120,8 +166,8 @@ export function formatActiveRunLines(
|
|
|
120
166
|
for (const root of roots) {
|
|
121
167
|
const children = childrenOf.get(root.id) ?? [];
|
|
122
168
|
lines.push(runPrimaryLine(root, theme, width, now, ""));
|
|
123
|
-
// The parent's
|
|
124
|
-
//
|
|
169
|
+
// The parent's managed-workflow placeholder is redundant while a child
|
|
170
|
+
// row shows live progress; keep it only between stages.
|
|
125
171
|
if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
|
|
126
172
|
children.forEach((child, index) => {
|
|
127
173
|
const connector = index === children.length - 1 ? "└ " : "├ ";
|
|
@@ -134,7 +180,7 @@ export function formatActiveRunLines(
|
|
|
134
180
|
|
|
135
181
|
function hasTickingRun(): boolean {
|
|
136
182
|
return monitor.getRuns().some(
|
|
137
|
-
(run) => isRunActiveStatus(run.status) && run.
|
|
183
|
+
(run) => isRunActiveStatus(run.status) && run.activeSince !== undefined,
|
|
138
184
|
);
|
|
139
185
|
}
|
|
140
186
|
|
package/src/worktree.ts
CHANGED
|
@@ -298,11 +298,18 @@ export function isPathInside(root: string, candidate: string): boolean {
|
|
|
298
298
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
interface RepositoryLocation {
|
|
302
|
+
originalCwd: string;
|
|
303
|
+
originalRoot: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Resolve the canonical repository root without requiring a committed HEAD.
|
|
307
|
+
* Managed repository lanes use this for empty repositories as well as normal
|
|
308
|
+
* worktrees; worktree creation validates HEAD separately below. */
|
|
309
|
+
async function resolveRepositoryLocation(
|
|
303
310
|
cwd: string,
|
|
304
|
-
runner: CommandRunner
|
|
305
|
-
): Promise<
|
|
311
|
+
runner: CommandRunner,
|
|
312
|
+
): Promise<RepositoryLocation> {
|
|
306
313
|
const requested = resolve(cwd);
|
|
307
314
|
try {
|
|
308
315
|
if (!(await stat(requested)).isDirectory()) throw new Error("not a directory");
|
|
@@ -335,6 +342,22 @@ export async function resolveWorktreeTarget(
|
|
|
335
342
|
`Requested cwd ${originalCwd} is not inside Git worktree root ${originalRoot}.`,
|
|
336
343
|
);
|
|
337
344
|
}
|
|
345
|
+
return { originalCwd, originalRoot };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function resolveRepositoryRoot(
|
|
349
|
+
cwd: string,
|
|
350
|
+
runner: CommandRunner = runCommand,
|
|
351
|
+
): Promise<string> {
|
|
352
|
+
return (await resolveRepositoryLocation(cwd, runner)).originalRoot;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Resolve and validate the Git repository/worktree that contains cwd. */
|
|
356
|
+
export async function resolveWorktreeTarget(
|
|
357
|
+
cwd: string,
|
|
358
|
+
runner: CommandRunner = runCommand,
|
|
359
|
+
): Promise<WorktreeTarget> {
|
|
360
|
+
const { originalCwd, originalRoot } = await resolveRepositoryLocation(cwd, runner);
|
|
338
361
|
let headResult: CommandResult;
|
|
339
362
|
try {
|
|
340
363
|
headResult = await runGit(
|