pi-prompt-template-model 0.6.5 → 0.6.7
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 +26 -1
- package/README.md +80 -3
- package/args.ts +23 -1
- package/chain-parser.ts +19 -12
- package/index.ts +81 -30
- package/loop-utils.ts +8 -0
- package/package.json +1 -1
- package/prompt-loader.ts +94 -8
- package/subagent-runtime.ts +13 -10
- package/subagent-step.ts +99 -10
- package/subagent-widget.ts +78 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.6.7] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Delegation progress widget now shows a unified tool stream where completed tools scroll chronologically with the active tool highlighted at the bottom, replacing the old design where the active tool flashed at the top and disappeared on completion.
|
|
7
|
+
- Delegation progress widget now renders the subagent's model name in the header (e.g., `delegate [fork] gpt-5.3-codex | 14 tools, 170k tok, 2m47s`).
|
|
8
|
+
- Delegation progress widget no longer caps tool history or output lines. The box grows to show the full execution trace.
|
|
9
|
+
- Delegation progress widget now refreshes every second during idle periods (model thinking) so the elapsed timer ticks smoothly instead of freezing between progress updates.
|
|
10
|
+
- Enriched the delegation bridge protocol to pass through full `recentOutputLines`, `recentTools` history, and `model` from pi-subagents progress data, replacing the old single-line `recentOutput` and missing tool/model fields.
|
|
11
|
+
- Removed `lastTool`/`lastToolArgs` tracking from live state (dead code after the unified tool stream redesign).
|
|
12
|
+
|
|
13
|
+
## [0.6.6] - 2026-03-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Added `--model=provider/model-id` runtime flag to override a template's model for a single invocation. Works with single execution, loops, and delegation.
|
|
17
|
+
- Added `--fork` runtime flag to enable `inheritContext` (forked context) at invocation time. Implies `--subagent` if not already set.
|
|
18
|
+
- Inline loop iterations now include a `[Loop 2/5]` prefix so the agent knows it's in a loop and which iteration it's on. Delegated (subagent) loops are unaffected.
|
|
19
|
+
- Added `loop: unlimited` (and `loop: true`) frontmatter for open-ended loops that run until convergence, user interrupt, or the 999-iteration safety cap.
|
|
20
|
+
- Added model rotation for loop iterations via `rotate: true` frontmatter. Cycles through comma-separated models and thinking levels each iteration instead of using fallback semantics.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Pressing Escape during a loop or chain iteration now stops the loop. Previously, aborted inline turns were treated as "no changes" and the loop continued.
|
|
24
|
+
- Delegation errors during loop iterations no longer abort the entire loop. The error is reported and the loop continues to the next iteration (useful with model rotation where one model may fail but others succeed).
|
|
25
|
+
- Per-step bare `--loop` in chain declarations (e.g., `double-check --loop -> deslop`) now correctly runs unlimited iterations instead of running once.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Unlimited loops (`--loop` bare or `loop: unlimited`) no longer force convergence on. Convergence follows the `converge` field like bounded loops. Safety cap raised from 50 to 999.
|
|
4
29
|
|
|
5
30
|
## [0.6.5] - 2026-03-24
|
|
6
31
|
|
package/README.md
CHANGED
|
@@ -69,7 +69,8 @@ All fields are optional. Templates that don't use any extension features (no `mo
|
|
|
69
69
|
| Field | Default | What it does |
|
|
70
70
|
|-------|---------|--------------|
|
|
71
71
|
| `restore` | `true` | After the command finishes, switch back to whatever model and thinking level were active before. Set `false` to stay on the new model. |
|
|
72
|
-
| `loop` | — | Run this template multiple times by default (1–999). CLI `--loop` overrides this. See [Loop Execution](#loop-execution). |
|
|
72
|
+
| `loop` | — | Run this template multiple times by default (1–999, `true`, or `unlimited`). CLI `--loop` overrides this. See [Loop Execution](#loop-execution). |
|
|
73
|
+
| `rotate` | `false` | When `true` and looping, cycle through models in the `model` list instead of using fallback semantics. Thinking levels can also be comma-separated to pair with each model. |
|
|
73
74
|
| `fresh` | `false` | When looping, collapse the conversation between iterations to a brief summary instead of carrying the full context forward. Saves tokens on long loops. |
|
|
74
75
|
| `converge` | `true` | When looping, stop early if an iteration makes no file changes. Set `false` to always run every iteration. |
|
|
75
76
|
|
|
@@ -241,6 +242,17 @@ During execution, a live progress widget appears above the editor showing elapse
|
|
|
241
242
|
|
|
242
243
|
You can override delegation at runtime per invocation with `--subagent`, `--subagent=<name>`, `--subagent:<name>`, or `--cwd=<path>`. `--cwd=<path>` must be absolute after optional `~/...` expansion. Runtime flags take precedence for that invocation only.
|
|
243
244
|
|
|
245
|
+
Two additional runtime flags work for any prompt (not just delegated ones):
|
|
246
|
+
|
|
247
|
+
- `--model=provider/model-id` — override the template's `model` for this invocation. Works with single execution, loops, and delegation.
|
|
248
|
+
- `--fork` — run with `inheritContext` (forked context). Implies `--subagent` if not already set.
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
/double-check --model=anthropic/claude-opus-4-6
|
|
252
|
+
/double-check --fork --subagent:worker
|
|
253
|
+
/deslop --model=openai/gpt-5.4 --loop 3
|
|
254
|
+
```
|
|
255
|
+
|
|
244
256
|
## Loop Execution
|
|
245
257
|
|
|
246
258
|
Run a template multiple times with `--loop`:
|
|
@@ -248,7 +260,7 @@ Run a template multiple times with `--loop`:
|
|
|
248
260
|
```
|
|
249
261
|
/deslop --loop 5
|
|
250
262
|
/deslop --loop=5
|
|
251
|
-
/deslop --loop # unlimited — runs until convergence
|
|
263
|
+
/deslop --loop # unlimited — runs until convergence or cap (999)
|
|
252
264
|
```
|
|
253
265
|
|
|
254
266
|
You can also set a default in frontmatter. CLI `--loop` always overrides:
|
|
@@ -259,11 +271,22 @@ loop: 5
|
|
|
259
271
|
---
|
|
260
272
|
```
|
|
261
273
|
|
|
274
|
+
Use `loop: unlimited` (or `loop: true`) for open-ended loops that run until convergence, user interrupt, or the safety cap of 999 iterations:
|
|
275
|
+
|
|
276
|
+
```markdown
|
|
277
|
+
---
|
|
278
|
+
loop: unlimited
|
|
279
|
+
converge: false
|
|
280
|
+
fresh: true
|
|
281
|
+
subagent: true
|
|
282
|
+
---
|
|
283
|
+
```
|
|
284
|
+
|
|
262
285
|
### How looping works
|
|
263
286
|
|
|
264
287
|
Each iteration runs the same prompt. By default, context accumulates — iteration 3 sees the full conversation from iterations 1 and 2 and builds on that work.
|
|
265
288
|
|
|
266
|
-
**Convergence**: If an iteration makes no file changes (no `write` or `edit` tool calls), the loop stops early. This is on by default. Use `--no-converge` or `converge: false` to always run every iteration.
|
|
289
|
+
**Convergence**: If an iteration makes no file changes (no `write` or `edit` tool calls), the loop stops early. This is on by default. Use `--no-converge` or `converge: false` to always run every iteration.
|
|
267
290
|
|
|
268
291
|
**Fresh context**: Add `--fresh` (or `fresh: true` in frontmatter) to collapse the conversation between iterations. Each iteration gets a clean slate with only brief summaries of what previous iterations did. Good for long loops where accumulated context would blow up the token count.
|
|
269
292
|
|
|
@@ -271,6 +294,60 @@ Each iteration runs the same prompt. By default, context accumulates — iterati
|
|
|
271
294
|
|
|
272
295
|
Model, thinking level, and skill are maintained throughout. If `restore: true` (the default), everything is restored after the final iteration.
|
|
273
296
|
|
|
297
|
+
## Model Rotation
|
|
298
|
+
|
|
299
|
+
`rotate: true` turns a comma-separated `model` list from a fallback chain into a cycling list. Each loop iteration uses the next model in the list, wrapping around:
|
|
300
|
+
|
|
301
|
+
```markdown
|
|
302
|
+
---
|
|
303
|
+
model: claude-opus-4-6, gpt-5.4, gpt-5.3-codex
|
|
304
|
+
thinking: high, xhigh, off
|
|
305
|
+
loop: 9
|
|
306
|
+
rotate: true
|
|
307
|
+
fresh: true
|
|
308
|
+
---
|
|
309
|
+
Review and fix issues in this codebase.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Iteration 1 runs Opus + `high`, iteration 2 runs GPT-5.4 + `xhigh`, iteration 3 runs Codex + `off`, then wraps back to Opus. The status bar shows which model is active: `loop 2/9 · gpt-5.4 xhigh`.
|
|
313
|
+
|
|
314
|
+
This is especially useful for [ralph-style loops](https://ghuntley.com/ralph/) where different models catch different things. The `subagent` examples below require [pi-subagents](https://github.com/nicobailon/pi-subagents/). A single-model ralph loop that delegates with fresh context each iteration:
|
|
315
|
+
|
|
316
|
+
```markdown
|
|
317
|
+
---
|
|
318
|
+
model: claude-sonnet-4-20250514
|
|
319
|
+
subagent: true
|
|
320
|
+
inheritContext: true
|
|
321
|
+
loop: 5
|
|
322
|
+
fresh: true
|
|
323
|
+
---
|
|
324
|
+
Simplify this code: $@
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Add `rotate` and multiple models to cycle different perspectives on each pass:
|
|
328
|
+
|
|
329
|
+
```markdown
|
|
330
|
+
---
|
|
331
|
+
model: claude-opus-4-6, gpt-5.4, gpt-5.3-codex
|
|
332
|
+
thinking: xhigh, high, high
|
|
333
|
+
loop: 9
|
|
334
|
+
rotate: true
|
|
335
|
+
fresh: true
|
|
336
|
+
subagent: true
|
|
337
|
+
---
|
|
338
|
+
Review and fix issues in this codebase.
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Each iteration gets fresh context, a different model, and its own thinking level. Convergence stops the loop when an iteration makes no file changes — use `converge: false` to guarantee every model gets at least one shot.
|
|
342
|
+
|
|
343
|
+
`thinking` pairing with `rotate: true`:
|
|
344
|
+
|
|
345
|
+
- Single value (`thinking: high`) — applied to every model.
|
|
346
|
+
- Comma-separated (`thinking: high, xhigh, off`) — positional, must match the number of models.
|
|
347
|
+
- Omitted — each iteration inherits the session default.
|
|
348
|
+
|
|
349
|
+
Without `loop`, `rotate` has no effect and comma-separated `model` keeps normal fallback behavior.
|
|
350
|
+
|
|
274
351
|
## Chaining Templates
|
|
275
352
|
|
|
276
353
|
`/chain-prompts` runs multiple templates in sequence. Each step uses its own model, skill, and thinking level, while conversation context flows between them:
|
package/args.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface SubagentOverrideExtraction {
|
|
|
20
20
|
args: string;
|
|
21
21
|
override?: SubagentOverride;
|
|
22
22
|
cwd?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
fork?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function extractLoopCount(argsString: string): LoopExtraction | null {
|
|
@@ -113,7 +115,7 @@ export function extractLoopCount(argsString: string): LoopExtraction | null {
|
|
|
113
115
|
cleaned = cleaned.slice(0, start) + cleaned.slice(end);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
const converge =
|
|
118
|
+
const converge = !noConverge;
|
|
117
119
|
return { args: cleaned.trim(), loopCount, fresh, converge };
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -210,6 +212,8 @@ export function extractChainContextFlag(argsString: string): { args: string; cha
|
|
|
210
212
|
export function extractSubagentOverride(argsString: string): SubagentOverrideExtraction {
|
|
211
213
|
let override: SubagentOverride | undefined;
|
|
212
214
|
let cwdRaw: string | undefined;
|
|
215
|
+
let modelRaw: string | undefined;
|
|
216
|
+
let fork = false;
|
|
213
217
|
const tokensToRemove: Array<{ start: number; end: number }> = [];
|
|
214
218
|
|
|
215
219
|
let i = 0;
|
|
@@ -250,6 +254,20 @@ export function extractSubagentOverride(argsString: string): SubagentOverrideExt
|
|
|
250
254
|
tokensToRemove.push({ start: tokenStart, end: i });
|
|
251
255
|
const value = token.slice("--cwd=".length);
|
|
252
256
|
cwdRaw = value || undefined;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token.startsWith("--model=")) {
|
|
261
|
+
tokensToRemove.push({ start: tokenStart, end: i });
|
|
262
|
+
const value = token.slice("--model=".length);
|
|
263
|
+
modelRaw = value || undefined;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (token === "--fork") {
|
|
268
|
+
tokensToRemove.push({ start: tokenStart, end: i });
|
|
269
|
+
fork = true;
|
|
270
|
+
continue;
|
|
253
271
|
}
|
|
254
272
|
}
|
|
255
273
|
|
|
@@ -261,10 +279,14 @@ export function extractSubagentOverride(argsString: string): SubagentOverrideExt
|
|
|
261
279
|
cleaned = cleaned.slice(0, start) + cleaned.slice(end);
|
|
262
280
|
}
|
|
263
281
|
|
|
282
|
+
if (fork && !override) override = { enabled: true };
|
|
283
|
+
|
|
264
284
|
return {
|
|
265
285
|
args: cleaned.trim(),
|
|
266
286
|
...(override ? { override } : {}),
|
|
267
287
|
...(cwdRaw !== undefined ? { cwd: cwdRaw } : {}),
|
|
288
|
+
...(modelRaw !== undefined ? { model: modelRaw } : {}),
|
|
289
|
+
...(fork ? { fork: true } : {}),
|
|
268
290
|
};
|
|
269
291
|
}
|
|
270
292
|
|
package/chain-parser.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { parseCommandArgs } from "./args.js";
|
|
|
3
3
|
export interface ChainStep {
|
|
4
4
|
name: string;
|
|
5
5
|
args: string[];
|
|
6
|
-
loopCount?: number;
|
|
6
|
+
loopCount?: number | null;
|
|
7
7
|
withContext?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -81,11 +81,11 @@ function scanSegmentTokens(segment: string): SegmentToken[] {
|
|
|
81
81
|
return tokens;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number; withContext: boolean } {
|
|
84
|
+
function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number | null; withContext: boolean } {
|
|
85
85
|
const tokens = scanSegmentTokens(segment);
|
|
86
86
|
const loopTokenRanges: Array<{ start: number; end: number }> = [];
|
|
87
87
|
const withContextTokenRanges: Array<{ start: number; end: number }> = [];
|
|
88
|
-
let loopCount: number | undefined;
|
|
88
|
+
let loopCount: number | null | undefined;
|
|
89
89
|
let withContext = false;
|
|
90
90
|
|
|
91
91
|
for (let i = 1; i < tokens.length; i++) {
|
|
@@ -109,17 +109,24 @@ function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?
|
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (token.value === "--loop"
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
if (token.value === "--loop") {
|
|
113
|
+
loopTokenRanges.push({ start: token.start, end: token.end });
|
|
114
|
+
if (i + 1 < tokens.length) {
|
|
115
|
+
const next = tokens[i + 1];
|
|
116
|
+
if (!next.quoted && /^\d+$/.test(next.value)) {
|
|
117
|
+
loopTokenRanges.push({ start: next.start, end: next.end });
|
|
118
|
+
const parsed = parseInt(next.value, 10);
|
|
119
|
+
if (parsed >= 1 && parsed <= 999 && loopCount === undefined) {
|
|
120
|
+
loopCount = parsed;
|
|
121
|
+
}
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
119
124
|
}
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
125
|
}
|
|
126
|
+
if (loopCount === undefined) {
|
|
127
|
+
loopCount = null;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@m
|
|
|
3
3
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
4
4
|
import { extractChainContextFlag, extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
|
|
5
5
|
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
|
|
6
|
-
import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
|
|
6
|
+
import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
|
|
7
7
|
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
|
|
8
8
|
import { preparePromptExecution } from "./prompt-execution.js";
|
|
9
9
|
import { buildPromptCommandDescription, expandCwdPath, loadPromptsWithModel, readSkillContent, resolveSkillPath, type PromptWithModel } from "./prompt-loader.js";
|
|
@@ -16,6 +16,7 @@ import { renderDelegatedSubagentResult } from "./subagent-renderer.js";
|
|
|
16
16
|
interface LoopState {
|
|
17
17
|
currentIteration: number;
|
|
18
18
|
totalIterations: number | null;
|
|
19
|
+
rotationLabel?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface FreshCollapse {
|
|
@@ -58,7 +59,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
58
59
|
let accumulatedSummaries: string[] = [];
|
|
59
60
|
let lastDiagnostics = "";
|
|
60
61
|
let storedCommandCtx: ExtensionCommandContext | null = null;
|
|
61
|
-
const UNLIMITED_LOOP_CAP =
|
|
62
|
+
const UNLIMITED_LOOP_CAP = 999;
|
|
62
63
|
|
|
63
64
|
const toolManager = createToolManager(pi, {
|
|
64
65
|
isActive: () => !!(loopState || chainActive),
|
|
@@ -131,9 +132,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
131
132
|
|
|
132
133
|
for (const command of pi.getCommands()) {
|
|
133
134
|
if (command.source !== "skill") continue;
|
|
134
|
-
|
|
135
|
+
const sourceInfo = "sourceInfo" in command
|
|
136
|
+
? (command as { sourceInfo?: { path?: string } }).sourceInfo
|
|
137
|
+
: undefined;
|
|
138
|
+
if (!sourceInfo?.path) continue;
|
|
135
139
|
if (!candidates.has(command.name)) continue;
|
|
136
|
-
return
|
|
140
|
+
return sourceInfo.path;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
return undefined;
|
|
@@ -196,6 +200,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
196
200
|
override?: SubagentOverride,
|
|
197
201
|
inheritedModel?: Model<any>,
|
|
198
202
|
taskPreamble?: string,
|
|
203
|
+
loopContext?: string,
|
|
199
204
|
): Promise<PromptStepResult | "aborted"> {
|
|
200
205
|
if (shouldDelegatePrompt(prompt, override)) {
|
|
201
206
|
try {
|
|
@@ -216,7 +221,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
216
221
|
return { changed: delegated.changed };
|
|
217
222
|
} catch (error) {
|
|
218
223
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
219
|
-
return
|
|
224
|
+
return { changed: false };
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
|
|
@@ -258,10 +263,14 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
258
263
|
pendingSkillMessage = skillResolution.kind === "ready" ? skillResolution.message : undefined;
|
|
259
264
|
|
|
260
265
|
const startId = ctx.sessionManager.getLeafId();
|
|
261
|
-
|
|
266
|
+
const content = loopContext ? `[${loopContext}]\n\n${prepared.content}` : prepared.content;
|
|
267
|
+
pi.sendUserMessage(content);
|
|
262
268
|
await waitForTurnStart(ctx);
|
|
263
269
|
await ctx.waitForIdle();
|
|
264
|
-
|
|
270
|
+
|
|
271
|
+
const entries = getIterationEntries(ctx, startId);
|
|
272
|
+
if (wasIterationAborted(entries)) return "aborted";
|
|
273
|
+
return { changed: didIterationMakeChanges(entries) };
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
async function restoreSessionState(
|
|
@@ -353,10 +362,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
353
362
|
function updateLoopStatus(ctx: ExtensionContext) {
|
|
354
363
|
if (!ctx.hasUI) return;
|
|
355
364
|
if (loopState) {
|
|
365
|
+
const suffix = loopState.rotationLabel ? ` · ${loopState.rotationLabel}` : "";
|
|
356
366
|
const label =
|
|
357
367
|
loopState.totalIterations !== null
|
|
358
|
-
? `loop ${loopState.currentIteration}/${loopState.totalIterations}`
|
|
359
|
-
: `loop ${loopState.currentIteration}`;
|
|
368
|
+
? `loop ${loopState.currentIteration}/${loopState.totalIterations}${suffix}`
|
|
369
|
+
: `loop ${loopState.currentIteration}${suffix}`;
|
|
360
370
|
ctx.ui.setStatus("prompt-loop", ctx.ui.theme.fg("warning", label));
|
|
361
371
|
} else {
|
|
362
372
|
ctx.ui.setStatus("prompt-loop", undefined);
|
|
@@ -385,6 +395,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
385
395
|
ctx: ExtensionCommandContext,
|
|
386
396
|
subagentOverride?: SubagentOverride,
|
|
387
397
|
cwdOverride?: string,
|
|
398
|
+
promptOverrides?: Partial<Pick<PromptWithModel, "models" | "inheritContext">>,
|
|
388
399
|
) {
|
|
389
400
|
refreshPrompts(ctx.cwd, ctx);
|
|
390
401
|
const initialPrompt = prompts.get(name);
|
|
@@ -401,7 +412,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
401
412
|
const useFresh = freshFlag || initialPrompt.fresh === true;
|
|
402
413
|
const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
|
|
403
414
|
const isUnlimited = totalIterations === null;
|
|
404
|
-
const useConverge =
|
|
415
|
+
const useConverge = converge && initialPrompt.converge !== false;
|
|
405
416
|
const anchorId = useFresh ? ctx.sessionManager.getLeafId() : null;
|
|
406
417
|
|
|
407
418
|
loopState = { currentIteration: 1, totalIterations };
|
|
@@ -414,9 +425,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
414
425
|
try {
|
|
415
426
|
for (let i = 0; i < effectiveMax; i++) {
|
|
416
427
|
loopState.currentIteration = i + 1;
|
|
417
|
-
updateLoopStatus(ctx);
|
|
418
428
|
const iterationLabel = totalIterations !== null ? `${i + 1}/${totalIterations}` : `${i + 1}`;
|
|
419
|
-
notify(ctx, `Loop ${iterationLabel}: ${name}`, "info");
|
|
420
429
|
|
|
421
430
|
refreshPrompts(ctx.cwd, ctx);
|
|
422
431
|
const prompt = prompts.get(name);
|
|
@@ -424,15 +433,40 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
424
433
|
notify(ctx, `Prompt "${name}" no longer exists`, "error");
|
|
425
434
|
break;
|
|
426
435
|
}
|
|
427
|
-
const effectivePrompt = cwdOverride ? {
|
|
436
|
+
const effectivePrompt = { ...prompt, ...(cwdOverride ? { cwd: cwdOverride } : {}), ...promptOverrides };
|
|
437
|
+
let iterationPrompt = effectivePrompt;
|
|
438
|
+
loopState!.rotationLabel = undefined;
|
|
439
|
+
if (effectivePrompt.rotate && effectivePrompt.models.length > 1) {
|
|
440
|
+
const rotationIndex = i % effectivePrompt.models.length;
|
|
441
|
+
const rotatedThinking = effectivePrompt.thinkingLevels
|
|
442
|
+
? effectivePrompt.thinkingLevels[rotationIndex]
|
|
443
|
+
: effectivePrompt.thinking;
|
|
444
|
+
iterationPrompt = {
|
|
445
|
+
...effectivePrompt,
|
|
446
|
+
models: [effectivePrompt.models[rotationIndex]],
|
|
447
|
+
thinking: rotatedThinking,
|
|
448
|
+
};
|
|
449
|
+
const shortModel = effectivePrompt.models[rotationIndex].split("/").pop() || effectivePrompt.models[rotationIndex];
|
|
450
|
+
const thinkingLabel = rotatedThinking ? ` ${rotatedThinking}` : "";
|
|
451
|
+
loopState!.rotationLabel = `${shortModel}${thinkingLabel}`;
|
|
452
|
+
}
|
|
453
|
+
updateLoopStatus(ctx);
|
|
454
|
+
const rotationSuffix = loopState!.rotationLabel ? ` [${loopState!.rotationLabel}]` : "";
|
|
455
|
+
notify(ctx, `Loop ${iterationLabel}: ${name}${rotationSuffix}`, "info");
|
|
428
456
|
|
|
457
|
+
const loopContext = loopState!.rotationLabel
|
|
458
|
+
? `Loop ${iterationLabel} · ${loopState!.rotationLabel}`
|
|
459
|
+
: `Loop ${iterationLabel}`;
|
|
429
460
|
const iterationStartId = ctx.sessionManager.getLeafId();
|
|
430
461
|
const stepResult = await executePromptStep(
|
|
431
|
-
|
|
462
|
+
iterationPrompt,
|
|
432
463
|
parseCommandArgs(cleanedArgs),
|
|
433
464
|
ctx,
|
|
434
465
|
currentModel,
|
|
435
466
|
subagentOverride,
|
|
467
|
+
undefined,
|
|
468
|
+
undefined,
|
|
469
|
+
loopContext,
|
|
436
470
|
);
|
|
437
471
|
if (stepResult === "aborted") break;
|
|
438
472
|
|
|
@@ -440,7 +474,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
440
474
|
currentThinking = pi.getThinkingLevel();
|
|
441
475
|
completedIterations++;
|
|
442
476
|
|
|
443
|
-
const iterationChanged = shouldDelegatePrompt(
|
|
477
|
+
const iterationChanged = shouldDelegatePrompt(iterationPrompt, subagentOverride)
|
|
444
478
|
? stepResult.changed
|
|
445
479
|
: didIterationMakeChanges(getIterationEntries(ctx, iterationStartId));
|
|
446
480
|
if (useConverge && (isUnlimited || effectiveMax > 1) && !iterationChanged) {
|
|
@@ -567,7 +601,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
567
601
|
pendingSkillMessage = undefined;
|
|
568
602
|
const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
|
|
569
603
|
const isUnlimited = totalIterations === null;
|
|
570
|
-
const useConverge =
|
|
604
|
+
const useConverge = converge;
|
|
571
605
|
|
|
572
606
|
const anchorId = fresh ? ctx.sessionManager.getLeafId() : null;
|
|
573
607
|
const chainStepNames = steps
|
|
@@ -612,7 +646,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
612
646
|
...(cwdOverride ? { cwd: cwdOverride } : {}),
|
|
613
647
|
},
|
|
614
648
|
stepArgs: step.args,
|
|
615
|
-
stepLoop: step.loopCount
|
|
649
|
+
stepLoop: step.loopCount !== undefined ? step.loopCount : 1,
|
|
616
650
|
stepWithContext: step.withContext === true,
|
|
617
651
|
},
|
|
618
652
|
},
|
|
@@ -676,7 +710,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
676
710
|
}
|
|
677
711
|
|
|
678
712
|
const singleStep = stepTemplate.singleStep;
|
|
679
|
-
const
|
|
713
|
+
const stepLoopTotal = singleStep.stepLoop;
|
|
714
|
+
const stepLoopMax = stepLoopTotal ?? UNLIMITED_LOOP_CAP;
|
|
715
|
+
const isStepLooping = stepLoopMax > 1;
|
|
680
716
|
const effectiveArgs = singleStep.stepArgs.length > 0 ? singleStep.stepArgs : sharedArgs;
|
|
681
717
|
const shouldInjectSummary =
|
|
682
718
|
shouldDelegatePrompt(singleStep.prompt, subagentOverride) &&
|
|
@@ -684,19 +720,23 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
684
720
|
(chainContextEnabled || singleStep.stepWithContext === true);
|
|
685
721
|
const outerLoopState = loopState ? { ...loopState } : null;
|
|
686
722
|
const stepStartId = ctx.sessionManager.getLeafId();
|
|
687
|
-
if (
|
|
688
|
-
loopState = { currentIteration: 1, totalIterations:
|
|
723
|
+
if (isStepLooping) {
|
|
724
|
+
loopState = { currentIteration: 1, totalIterations: stepLoopTotal };
|
|
689
725
|
updateLoopStatus(ctx);
|
|
690
726
|
}
|
|
691
727
|
|
|
692
728
|
try {
|
|
693
|
-
for (let stepIteration = 0; stepIteration <
|
|
694
|
-
if (
|
|
695
|
-
loopState = { currentIteration: stepIteration + 1, totalIterations:
|
|
729
|
+
for (let stepIteration = 0; stepIteration < stepLoopMax; stepIteration++) {
|
|
730
|
+
if (isStepLooping) {
|
|
731
|
+
loopState = { currentIteration: stepIteration + 1, totalIterations: stepLoopTotal };
|
|
696
732
|
updateLoopStatus(ctx);
|
|
697
733
|
}
|
|
698
734
|
|
|
699
|
-
const iterSuffix =
|
|
735
|
+
const iterSuffix = isStepLooping
|
|
736
|
+
? stepLoopTotal !== null
|
|
737
|
+
? ` (iter ${stepIteration + 1}/${stepLoopTotal})`
|
|
738
|
+
: ` (iter ${stepIteration + 1})`
|
|
739
|
+
: "";
|
|
700
740
|
notify(
|
|
701
741
|
ctx,
|
|
702
742
|
`${loopPrefix}Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix} ${buildPromptCommandDescription(singleStep.prompt)}`,
|
|
@@ -709,6 +749,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
709
749
|
? `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`
|
|
710
750
|
: undefined;
|
|
711
751
|
|
|
752
|
+
const stepLoopContext = isStepLooping
|
|
753
|
+
? `Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix}`
|
|
754
|
+
: undefined;
|
|
712
755
|
const stepIterationStartId = ctx.sessionManager.getLeafId();
|
|
713
756
|
const stepResult = await executePromptStep(
|
|
714
757
|
singleStep.prompt,
|
|
@@ -718,6 +761,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
718
761
|
subagentOverride,
|
|
719
762
|
chainInheritedModel,
|
|
720
763
|
taskPreamble,
|
|
764
|
+
stepLoopContext,
|
|
721
765
|
);
|
|
722
766
|
if (stepResult === "aborted") {
|
|
723
767
|
aborted = true;
|
|
@@ -729,12 +773,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
729
773
|
|
|
730
774
|
const stepIterationEntries = getIterationEntries(ctx, stepIterationStartId);
|
|
731
775
|
const stepIterationChanged = didIterationMakeChanges(stepIterationEntries);
|
|
732
|
-
if (
|
|
776
|
+
if (isStepLooping && singleStep.prompt.converge !== false && !stepIterationChanged) {
|
|
733
777
|
break;
|
|
734
778
|
}
|
|
735
779
|
}
|
|
736
780
|
} finally {
|
|
737
|
-
if (
|
|
781
|
+
if (isStepLooping) {
|
|
738
782
|
loopState = outerLoopState ? { ...outerLoopState } : null;
|
|
739
783
|
updateLoopStatus(ctx);
|
|
740
784
|
}
|
|
@@ -817,10 +861,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
817
861
|
const argsWithoutSubagent = subagent.args;
|
|
818
862
|
|
|
819
863
|
if (prompt.chain) {
|
|
864
|
+
if (subagent.model) notify(ctx, `--model is not supported on chain prompts (ignored)`, "warning");
|
|
865
|
+
if (subagent.fork) notify(ctx, `--fork is not supported on chain prompts (ignored)`, "warning");
|
|
820
866
|
const extracted = extractChainContextFlag(argsWithoutSubagent);
|
|
821
867
|
const chainContextEnabled = extracted.chainContext || prompt.chainContext === "summary";
|
|
822
868
|
const loop = extractLoopCount(extracted.args);
|
|
823
|
-
let totalIterations: number | null = prompt.loop
|
|
869
|
+
let totalIterations: number | null = prompt.loop !== undefined ? prompt.loop : 1;
|
|
824
870
|
let fresh = false;
|
|
825
871
|
let converge = true;
|
|
826
872
|
let cleanedArgs = extracted.args;
|
|
@@ -863,19 +909,24 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
863
909
|
return;
|
|
864
910
|
}
|
|
865
911
|
|
|
912
|
+
const promptOverrides: Partial<Pick<PromptWithModel, "models" | "inheritContext">> = {
|
|
913
|
+
...(subagent.model ? { models: [subagent.model] } : {}),
|
|
914
|
+
...(subagent.fork ? { inheritContext: true } : {}),
|
|
915
|
+
};
|
|
916
|
+
|
|
866
917
|
const loop = extractLoopCount(argsWithoutSubagent);
|
|
867
918
|
if (loop) {
|
|
868
|
-
await runPromptLoop(name, loop.args, loop.loopCount, loop.fresh, loop.converge, ctx, subagent.override, runtimeCwd);
|
|
919
|
+
await runPromptLoop(name, loop.args, loop.loopCount, loop.fresh, loop.converge, ctx, subagent.override, runtimeCwd, promptOverrides);
|
|
869
920
|
return;
|
|
870
921
|
}
|
|
871
922
|
|
|
872
923
|
if (prompt.loop !== undefined) {
|
|
873
924
|
const flags = extractLoopFlags(argsWithoutSubagent);
|
|
874
|
-
await runPromptLoop(name, flags.args, prompt.loop, flags.fresh, flags.converge, ctx, subagent.override, runtimeCwd);
|
|
925
|
+
await runPromptLoop(name, flags.args, prompt.loop, flags.fresh, flags.converge, ctx, subagent.override, runtimeCwd, promptOverrides);
|
|
875
926
|
return;
|
|
876
927
|
}
|
|
877
928
|
|
|
878
|
-
const effectivePrompt = runtimeCwd ? {
|
|
929
|
+
const effectivePrompt = { ...prompt, ...(runtimeCwd ? { cwd: runtimeCwd } : {}), ...promptOverrides };
|
|
879
930
|
const savedModel = getCurrentModel(ctx);
|
|
880
931
|
const savedThinking = pi.getThinkingLevel();
|
|
881
932
|
const stepResult = await executePromptStep(
|
package/loop-utils.ts
CHANGED
|
@@ -153,3 +153,11 @@ export function getIterationEntries(ctx: Pick<ExtensionContext, "sessionManager"
|
|
|
153
153
|
if (startIdx < 0) return branch;
|
|
154
154
|
return branch.slice(startIdx + 1);
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export function wasIterationAborted(entries: SessionEntry[]): boolean {
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
160
|
+
if ((entry.message as AssistantMessage).stopReason === "aborted") return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
package/package.json
CHANGED
package/prompt-loader.ts
CHANGED
|
@@ -42,8 +42,10 @@ export interface PromptWithModel {
|
|
|
42
42
|
restore: boolean;
|
|
43
43
|
skill?: string;
|
|
44
44
|
thinking?: ThinkingLevel;
|
|
45
|
+
thinkingLevels?: ThinkingLevel[];
|
|
46
|
+
rotate?: boolean;
|
|
45
47
|
fresh?: boolean;
|
|
46
|
-
loop?: number;
|
|
48
|
+
loop?: number | null;
|
|
47
49
|
converge?: boolean;
|
|
48
50
|
subagent?: true | string;
|
|
49
51
|
inheritContext?: boolean;
|
|
@@ -243,14 +245,43 @@ function normalizeFresh(
|
|
|
243
245
|
return false;
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
function normalizeRotate(
|
|
249
|
+
value: unknown,
|
|
250
|
+
filePath: string,
|
|
251
|
+
source: PromptSource,
|
|
252
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
253
|
+
): boolean {
|
|
254
|
+
if (value === undefined) return false;
|
|
255
|
+
if (typeof value === "boolean") return value;
|
|
256
|
+
if (typeof value === "string") {
|
|
257
|
+
const normalized = value.trim().toLowerCase();
|
|
258
|
+
if (normalized === "true") return true;
|
|
259
|
+
if (normalized === "false") return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
diagnostics.push(
|
|
263
|
+
createDiagnostic(
|
|
264
|
+
"invalid-rotate",
|
|
265
|
+
filePath,
|
|
266
|
+
source,
|
|
267
|
+
`Using default rotate=false for ${filePath}: frontmatter field "rotate" must be true or false.`,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
function normalizeLoop(
|
|
247
274
|
value: unknown,
|
|
248
275
|
filePath: string,
|
|
249
276
|
source: PromptSource,
|
|
250
277
|
diagnostics: PromptLoaderDiagnostic[],
|
|
251
|
-
): number | undefined {
|
|
278
|
+
): number | null | undefined {
|
|
252
279
|
if (value === undefined) return undefined;
|
|
253
280
|
|
|
281
|
+
if (value === true || (typeof value === "string" && value.trim().toLowerCase() === "unlimited")) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
let normalizedValue: number | undefined;
|
|
255
286
|
if (typeof value === "number") {
|
|
256
287
|
normalizedValue = value;
|
|
@@ -267,7 +298,7 @@ function normalizeLoop(
|
|
|
267
298
|
"invalid-loop",
|
|
268
299
|
filePath,
|
|
269
300
|
source,
|
|
270
|
-
`Ignoring invalid loop value in ${filePath}: frontmatter field "loop" must be an integer between 1 and 999.`,
|
|
301
|
+
`Ignoring invalid loop value in ${filePath}: frontmatter field "loop" must be an integer between 1 and 999, true, or "unlimited".`,
|
|
271
302
|
),
|
|
272
303
|
);
|
|
273
304
|
return undefined;
|
|
@@ -481,6 +512,48 @@ function normalizeThinking(
|
|
|
481
512
|
return undefined;
|
|
482
513
|
}
|
|
483
514
|
|
|
515
|
+
function normalizeThinkingLevels(
|
|
516
|
+
value: unknown,
|
|
517
|
+
modelCount: number,
|
|
518
|
+
filePath: string,
|
|
519
|
+
source: PromptSource,
|
|
520
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
521
|
+
): ThinkingLevel[] | undefined {
|
|
522
|
+
if (typeof value !== "string") return undefined;
|
|
523
|
+
|
|
524
|
+
const levels = value
|
|
525
|
+
.split(",")
|
|
526
|
+
.map((item) => item.trim())
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
|
|
529
|
+
const invalidLevel = levels.find((level) => !(VALID_THINKING_LEVELS as readonly string[]).includes(level.toLowerCase()));
|
|
530
|
+
if (invalidLevel) {
|
|
531
|
+
diagnostics.push(
|
|
532
|
+
createDiagnostic(
|
|
533
|
+
"invalid-thinking-levels",
|
|
534
|
+
filePath,
|
|
535
|
+
source,
|
|
536
|
+
`Ignoring invalid thinking level in ${filePath}: ${JSON.stringify(invalidLevel)}.`,
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (levels.length !== modelCount) {
|
|
543
|
+
diagnostics.push(
|
|
544
|
+
createDiagnostic(
|
|
545
|
+
"invalid-thinking-level-count",
|
|
546
|
+
filePath,
|
|
547
|
+
source,
|
|
548
|
+
`Ignoring comma-separated thinking levels in ${filePath}: expected ${modelCount} entries to match frontmatter field "model".`,
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return levels.map((level) => level.toLowerCase() as ThinkingLevel);
|
|
555
|
+
}
|
|
556
|
+
|
|
484
557
|
function loadPromptsWithModelFromDir(
|
|
485
558
|
dir: string,
|
|
486
559
|
source: PromptSource,
|
|
@@ -619,6 +692,7 @@ function loadPromptsWithModelFromDir(
|
|
|
619
692
|
const parsedModels = chain ? [] : normalizeModelSpecs(frontmatter.model, fullPath, source, diagnostics);
|
|
620
693
|
if (!chain && hasModelField && !parsedModels) continue;
|
|
621
694
|
const models = chain ? [] : (parsedModels ?? []);
|
|
695
|
+
const rotate = chain ? false : normalizeRotate(frontmatter.rotate, fullPath, source, diagnostics);
|
|
622
696
|
|
|
623
697
|
const name = entry.name.slice(0, -3);
|
|
624
698
|
if (RESERVED_COMMAND_NAMES.has(name)) {
|
|
@@ -637,7 +711,15 @@ function loadPromptsWithModelFromDir(
|
|
|
637
711
|
const safeCwd = (chain || subagent !== undefined) ? cwd : undefined;
|
|
638
712
|
const description = normalizeStringField("description", frontmatter.description, fullPath, source, diagnostics) ?? "";
|
|
639
713
|
const skill = chain ? undefined : normalizeStringField("skill", frontmatter.skill, fullPath, source, diagnostics);
|
|
640
|
-
|
|
714
|
+
let thinking: ThinkingLevel | undefined;
|
|
715
|
+
let thinkingLevels: ThinkingLevel[] | undefined;
|
|
716
|
+
if (!chain) {
|
|
717
|
+
if (rotate && typeof frontmatter.thinking === "string" && frontmatter.thinking.includes(",")) {
|
|
718
|
+
thinkingLevels = normalizeThinkingLevels(frontmatter.thinking, models.length, fullPath, source, diagnostics);
|
|
719
|
+
} else {
|
|
720
|
+
thinking = normalizeThinking(frontmatter.thinking, fullPath, source, diagnostics);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
641
723
|
const restore = normalizeRestore(frontmatter.restore, fullPath, source, diagnostics);
|
|
642
724
|
const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics);
|
|
643
725
|
const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics);
|
|
@@ -666,8 +748,10 @@ function loadPromptsWithModelFromDir(
|
|
|
666
748
|
restore,
|
|
667
749
|
skill,
|
|
668
750
|
thinking,
|
|
751
|
+
thinkingLevels,
|
|
752
|
+
rotate: rotate || undefined,
|
|
669
753
|
fresh: fresh || undefined,
|
|
670
|
-
loop: loop
|
|
754
|
+
loop: loop !== undefined ? loop : undefined,
|
|
671
755
|
converge: converge === false ? false : undefined,
|
|
672
756
|
subagent,
|
|
673
757
|
inheritContext: safeInheritContext || undefined,
|
|
@@ -753,13 +837,15 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
753
837
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
754
838
|
}
|
|
755
839
|
const modelLabel = prompt.models.length > 0 ? prompt.models.map((model) => model.split("/").pop() || model).join("|") : "current";
|
|
840
|
+
const rotateLabel = prompt.rotate ? " rotate" : "";
|
|
756
841
|
const skillLabel = prompt.skill ? ` +${prompt.skill}` : "";
|
|
757
|
-
const
|
|
758
|
-
const
|
|
842
|
+
const thinkingValue = prompt.thinkingLevels ? prompt.thinkingLevels.join(",") : prompt.thinking;
|
|
843
|
+
const thinkingLabel = thinkingValue ? ` ${thinkingValue}` : "";
|
|
844
|
+
const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : "";
|
|
759
845
|
const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : "";
|
|
760
846
|
const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : "";
|
|
761
847
|
const inheritContextLabel = prompt.inheritContext ? " fork" : "";
|
|
762
|
-
const details = `[${modelLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${cwdLabel}${inheritContextLabel}] ${sourceLabel}`;
|
|
848
|
+
const details = `[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${cwdLabel}${inheritContextLabel}] ${sourceLabel}`;
|
|
763
849
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
764
850
|
}
|
|
765
851
|
|
package/subagent-runtime.ts
CHANGED
|
@@ -52,6 +52,9 @@ export interface DelegatedSubagentUpdate {
|
|
|
52
52
|
currentTool?: string;
|
|
53
53
|
currentToolArgs?: string;
|
|
54
54
|
recentOutput?: string;
|
|
55
|
+
recentOutputLines?: string[];
|
|
56
|
+
recentTools?: Array<{ tool: string; args: string }>;
|
|
57
|
+
model?: string;
|
|
55
58
|
toolCount?: number;
|
|
56
59
|
durationMs?: number;
|
|
57
60
|
tokens?: number;
|
|
@@ -65,6 +68,9 @@ export interface DelegatedSubagentTaskProgress {
|
|
|
65
68
|
currentTool?: string;
|
|
66
69
|
currentToolArgs?: string;
|
|
67
70
|
recentOutput?: string;
|
|
71
|
+
recentOutputLines?: string[];
|
|
72
|
+
recentTools?: Array<{ tool: string; args: string }>;
|
|
73
|
+
model?: string;
|
|
68
74
|
toolCount?: number;
|
|
69
75
|
durationMs?: number;
|
|
70
76
|
tokens?: number;
|
|
@@ -74,9 +80,9 @@ export interface DelegatedSubagentLiveState {
|
|
|
74
80
|
status?: string;
|
|
75
81
|
currentTool?: string;
|
|
76
82
|
currentToolArgs?: string;
|
|
77
|
-
lastTool?: string;
|
|
78
|
-
lastToolArgs?: string;
|
|
79
83
|
recentOutput: string[];
|
|
84
|
+
recentTools: Array<{ tool: string; args: string }>;
|
|
85
|
+
model?: string;
|
|
80
86
|
toolCount: number;
|
|
81
87
|
durationMs: number;
|
|
82
88
|
tokens: number;
|
|
@@ -151,6 +157,7 @@ export function updateDelegatedLiveState(requestId: string, update: Partial<Dele
|
|
|
151
157
|
const now = Date.now();
|
|
152
158
|
const existing = delegatedLiveState.get(requestId) ?? {
|
|
153
159
|
recentOutput: [],
|
|
160
|
+
recentTools: [],
|
|
154
161
|
toolCount: 0,
|
|
155
162
|
durationMs: 0,
|
|
156
163
|
tokens: 0,
|
|
@@ -158,21 +165,16 @@ export function updateDelegatedLiveState(requestId: string, update: Partial<Dele
|
|
|
158
165
|
startedAt: now,
|
|
159
166
|
updatedAt: now,
|
|
160
167
|
};
|
|
161
|
-
// When a tool finishes (currentTool goes undefined), preserve it as lastTool
|
|
162
|
-
const toolJustCleared = update.currentTool === undefined && existing.currentTool !== undefined;
|
|
163
|
-
const lastTool = toolJustCleared ? existing.currentTool : (update.currentTool ?? existing.lastTool);
|
|
164
|
-
const lastToolArgs = toolJustCleared ? existing.currentToolArgs : (update.currentToolArgs ?? existing.lastToolArgs);
|
|
165
|
-
|
|
166
168
|
const next: DelegatedSubagentLiveState = {
|
|
167
169
|
...existing,
|
|
168
170
|
...update,
|
|
169
171
|
recentOutput: update.recentOutput ?? existing.recentOutput,
|
|
172
|
+
recentTools: update.recentTools ?? existing.recentTools,
|
|
173
|
+
model: update.model ?? existing.model,
|
|
170
174
|
toolCount: update.toolCount ?? existing.toolCount,
|
|
171
175
|
durationMs: update.durationMs ?? (now - existing.startedAt),
|
|
172
176
|
tokens: update.tokens ?? existing.tokens,
|
|
173
177
|
taskProgress: update.taskProgress ?? existing.taskProgress,
|
|
174
|
-
lastTool,
|
|
175
|
-
lastToolArgs,
|
|
176
178
|
startedAt: existing.startedAt,
|
|
177
179
|
updatedAt: now,
|
|
178
180
|
};
|
|
@@ -184,6 +186,7 @@ export function appendDelegatedLiveOutput(requestId: string, line?: string): voi
|
|
|
184
186
|
const fallbackNow = Date.now();
|
|
185
187
|
const existing = delegatedLiveState.get(requestId) ?? {
|
|
186
188
|
recentOutput: [],
|
|
189
|
+
recentTools: [],
|
|
187
190
|
toolCount: 0,
|
|
188
191
|
durationMs: 0,
|
|
189
192
|
tokens: 0,
|
|
@@ -191,7 +194,7 @@ export function appendDelegatedLiveOutput(requestId: string, line?: string): voi
|
|
|
191
194
|
startedAt: fallbackNow,
|
|
192
195
|
updatedAt: fallbackNow,
|
|
193
196
|
};
|
|
194
|
-
const recentOutput = [...existing.recentOutput, line]
|
|
197
|
+
const recentOutput = [...existing.recentOutput, line];
|
|
195
198
|
delegatedLiveState.set(requestId, {
|
|
196
199
|
...existing,
|
|
197
200
|
recentOutput,
|
package/subagent-step.ts
CHANGED
|
@@ -204,6 +204,9 @@ function formatProgressStatus(update: DelegatedSubagentUpdate): string | undefin
|
|
|
204
204
|
if (update.currentTool) {
|
|
205
205
|
return `running ${update.currentTool}${update.currentToolArgs ? ` ${update.currentToolArgs}` : ""}`;
|
|
206
206
|
}
|
|
207
|
+
if (update.taskProgress?.some((task) => task.status === "running")) {
|
|
208
|
+
return "running";
|
|
209
|
+
}
|
|
207
210
|
if (update.toolCount && update.toolCount > 0) {
|
|
208
211
|
return `completed ${update.toolCount} tool${update.toolCount === 1 ? "" : "s"}`;
|
|
209
212
|
}
|
|
@@ -216,6 +219,38 @@ function formatParallelProgressStatus(update: DelegatedSubagentUpdate): string |
|
|
|
216
219
|
return `parallel ${completed}/${update.taskProgress.length} running`;
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function hasOwn<T extends object>(value: T, key: PropertyKey): boolean {
|
|
223
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sanitizeOutputLines(lines: string[] | undefined): string[] {
|
|
227
|
+
if (!lines || lines.length === 0) return [];
|
|
228
|
+
return lines.filter((line): line is string => typeof line === "string" && line.trim() && line.trim() !== "(running...)");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function collectNewOutputLines(previous: string[] | undefined, next: string[] | undefined): string[] {
|
|
232
|
+
const previousLines = sanitizeOutputLines(previous);
|
|
233
|
+
const nextLines = sanitizeOutputLines(next);
|
|
234
|
+
if (nextLines.length === 0) return [];
|
|
235
|
+
if (previousLines.length === 0) return nextLines;
|
|
236
|
+
|
|
237
|
+
const overlapLimit = Math.min(previousLines.length, nextLines.length);
|
|
238
|
+
for (let overlap = overlapLimit; overlap > 0; overlap--) {
|
|
239
|
+
let matches = true;
|
|
240
|
+
for (let index = 0; index < overlap; index++) {
|
|
241
|
+
if (previousLines[previousLines.length - overlap + index] !== nextLines[index]) {
|
|
242
|
+
matches = false;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (matches) {
|
|
247
|
+
return nextLines.slice(overlap);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return nextLines;
|
|
252
|
+
}
|
|
253
|
+
|
|
219
254
|
function mergeTaskProgress(
|
|
220
255
|
requestTasks: DelegatedSubagentTask[] | undefined,
|
|
221
256
|
existingProgress: DelegatedSubagentTaskProgress[] | undefined,
|
|
@@ -235,6 +270,9 @@ function mergeTaskProgress(
|
|
|
235
270
|
currentTool: existing?.currentTool,
|
|
236
271
|
currentToolArgs: existing?.currentToolArgs,
|
|
237
272
|
recentOutput: existing?.recentOutput,
|
|
273
|
+
recentOutputLines: existing?.recentOutputLines,
|
|
274
|
+
recentTools: existing?.recentTools,
|
|
275
|
+
model: existing?.model ?? task.model,
|
|
238
276
|
toolCount: existing?.toolCount,
|
|
239
277
|
durationMs: existing?.durationMs,
|
|
240
278
|
tokens: existing?.tokens,
|
|
@@ -255,11 +293,20 @@ function mergeTaskProgress(
|
|
|
255
293
|
}
|
|
256
294
|
if (targetIndex < 0) continue;
|
|
257
295
|
consumed.add(targetIndex);
|
|
296
|
+
const current = merged[targetIndex]!;
|
|
258
297
|
merged[targetIndex] = {
|
|
259
|
-
...merged[targetIndex],
|
|
260
|
-
...entry,
|
|
261
298
|
index: targetIndex,
|
|
262
|
-
agent:
|
|
299
|
+
agent: current.agent,
|
|
300
|
+
status: entry.status ?? current.status,
|
|
301
|
+
currentTool: hasOwn(entry, "currentTool") ? entry.currentTool : current.currentTool,
|
|
302
|
+
currentToolArgs: hasOwn(entry, "currentToolArgs") ? entry.currentToolArgs : current.currentToolArgs,
|
|
303
|
+
recentOutput: entry.recentOutput ?? current.recentOutput,
|
|
304
|
+
recentOutputLines: entry.recentOutputLines ?? current.recentOutputLines,
|
|
305
|
+
recentTools: entry.recentTools ?? current.recentTools,
|
|
306
|
+
model: entry.model ?? current.model,
|
|
307
|
+
toolCount: entry.toolCount ?? current.toolCount,
|
|
308
|
+
durationMs: entry.durationMs ?? current.durationMs,
|
|
309
|
+
tokens: entry.tokens ?? current.tokens,
|
|
263
310
|
};
|
|
264
311
|
}
|
|
265
312
|
|
|
@@ -316,18 +363,29 @@ async function requestDelegatedRun(
|
|
|
316
363
|
|
|
317
364
|
let lastProgressStatus = "";
|
|
318
365
|
let widgetSet = false;
|
|
366
|
+
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
319
367
|
|
|
320
368
|
const showWidget = () => {
|
|
321
369
|
if (!ctx.hasUI || widgetSet) return;
|
|
322
370
|
widgetSet = true;
|
|
323
371
|
ctx.ui.setWidget(
|
|
324
372
|
DELEGATED_WIDGET_KEY,
|
|
325
|
-
(_tui, theme) => createDelegatedProgressWidget(request.requestId, request.agent, request.context, request.task, request.tasks, theme),
|
|
373
|
+
(_tui, theme) => createDelegatedProgressWidget(request.requestId, request.agent, request.context, request.task, request.tasks, theme, request.model),
|
|
326
374
|
{ placement: "aboveEditor" },
|
|
327
375
|
);
|
|
376
|
+
// Force TUI repaints every second so the elapsed timer ticks during idle periods
|
|
377
|
+
refreshTimer = setInterval(() => {
|
|
378
|
+
if (done) return;
|
|
379
|
+
const statusLine = lastProgressStatus || "running...";
|
|
380
|
+
ctx.ui.setStatus("prompt-subagent", `delegating to ${requestLabel} · ${statusLine}`);
|
|
381
|
+
}, 1000);
|
|
328
382
|
};
|
|
329
383
|
|
|
330
384
|
const clearWidget = () => {
|
|
385
|
+
if (refreshTimer) {
|
|
386
|
+
clearInterval(refreshTimer);
|
|
387
|
+
refreshTimer = null;
|
|
388
|
+
}
|
|
331
389
|
if (ctx.hasUI && widgetSet) {
|
|
332
390
|
ctx.ui.setWidget(DELEGATED_WIDGET_KEY, undefined);
|
|
333
391
|
widgetSet = false;
|
|
@@ -338,9 +396,11 @@ async function requestDelegatedRun(
|
|
|
338
396
|
if (done || !data || typeof data !== "object") return;
|
|
339
397
|
const update = data as DelegatedSubagentUpdate;
|
|
340
398
|
if (update.requestId !== request.requestId) return;
|
|
399
|
+
|
|
400
|
+
const previousTaskProgress = getDelegatedLiveState(request.requestId)?.taskProgress;
|
|
341
401
|
const mergedTaskProgress = mergeTaskProgress(
|
|
342
402
|
request.tasks,
|
|
343
|
-
|
|
403
|
+
previousTaskProgress,
|
|
344
404
|
update.taskProgress,
|
|
345
405
|
);
|
|
346
406
|
const isParallel = (request.tasks?.length ?? 0) > 0;
|
|
@@ -353,18 +413,46 @@ async function requestDelegatedRun(
|
|
|
353
413
|
if (progressStatus) {
|
|
354
414
|
lastProgressStatus = progressStatus;
|
|
355
415
|
}
|
|
416
|
+
|
|
356
417
|
updateDelegatedLiveState(request.requestId, {
|
|
357
418
|
status: progressStatus ?? (lastProgressStatus || "running..."),
|
|
358
419
|
currentTool: update.currentTool,
|
|
359
420
|
currentToolArgs: update.currentToolArgs,
|
|
421
|
+
recentTools: update.recentTools,
|
|
422
|
+
model: update.model,
|
|
360
423
|
toolCount: update.toolCount,
|
|
361
424
|
durationMs: update.durationMs,
|
|
362
425
|
tokens: update.tokens,
|
|
363
426
|
taskProgress: mergedTaskProgress,
|
|
364
427
|
});
|
|
365
|
-
|
|
366
|
-
if (
|
|
428
|
+
|
|
429
|
+
if (!isParallel) {
|
|
430
|
+
if (update.recentOutputLines && update.recentOutputLines.length > 0) {
|
|
431
|
+
updateDelegatedLiveState(request.requestId, {
|
|
432
|
+
recentOutput: sanitizeOutputLines(update.recentOutputLines),
|
|
433
|
+
});
|
|
434
|
+
} else {
|
|
435
|
+
appendDelegatedLiveOutput(request.requestId, update.recentOutput);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (isParallel && mergedTaskProgress) {
|
|
367
440
|
for (const task of mergedTaskProgress) {
|
|
441
|
+
const previousTask =
|
|
442
|
+
previousTaskProgress?.find((entry) => entry.index === task.index) ??
|
|
443
|
+
previousTaskProgress?.find((entry) => entry.agent === task.agent);
|
|
444
|
+
|
|
445
|
+
const newOutputLines = collectNewOutputLines(previousTask?.recentOutputLines, task.recentOutputLines);
|
|
446
|
+
if (newOutputLines.length > 0) {
|
|
447
|
+
for (const line of newOutputLines) {
|
|
448
|
+
appendDelegatedLiveOutput(request.requestId, line);
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!task.recentOutput || task.recentOutput === previousTask?.recentOutput) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
368
456
|
appendDelegatedLiveOutput(request.requestId, task.recentOutput);
|
|
369
457
|
}
|
|
370
458
|
}
|
|
@@ -567,11 +655,12 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
|
|
|
567
655
|
agent: preparedTasks[0]!.agent,
|
|
568
656
|
};
|
|
569
657
|
} catch (error) {
|
|
570
|
-
const
|
|
658
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
659
|
+
const responseText = cause.message;
|
|
571
660
|
if (isParallelRequest) {
|
|
572
|
-
throw new Error(`Parallel delegated prompts (${promptLabel}) failed: ${responseText}
|
|
661
|
+
throw new Error(`Parallel delegated prompts (${promptLabel}) failed: ${responseText}`, { cause });
|
|
573
662
|
}
|
|
574
|
-
throw new Error(`Prompt \`${preparedTasks[0]!.promptName}\` delegated subagent \`${preparedTasks[0]!.agent}\` failed: ${responseText}
|
|
663
|
+
throw new Error(`Prompt \`${preparedTasks[0]!.promptName}\` delegated subagent \`${preparedTasks[0]!.agent}\` failed: ${responseText}`, { cause });
|
|
575
664
|
} finally {
|
|
576
665
|
clearDelegatedLiveState(request.requestId);
|
|
577
666
|
if (ctx.hasUI) {
|
package/subagent-widget.ts
CHANGED
|
@@ -18,6 +18,28 @@ function formatTokens(n: number | undefined): string {
|
|
|
18
18
|
return String(n);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function normalizeModelLabel(model: string | undefined): string | undefined {
|
|
22
|
+
if (!model) return undefined;
|
|
23
|
+
return model.includes("/") ? model.split("/").pop() : model;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatToolCall(tool: string, args: string): string {
|
|
27
|
+
const safeArgs = args ?? "";
|
|
28
|
+
switch (tool) {
|
|
29
|
+
case "bash": {
|
|
30
|
+
const cmd = safeArgs.replace(/[\n\t]/g, " ").trim();
|
|
31
|
+
return `$ ${cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd}`;
|
|
32
|
+
}
|
|
33
|
+
case "read": return `[read: ${safeArgs}]`;
|
|
34
|
+
case "write": return `[write: ${safeArgs}]`;
|
|
35
|
+
case "edit": return `[edit: ${safeArgs}]`;
|
|
36
|
+
default: {
|
|
37
|
+
const short = safeArgs.length > 60 ? safeArgs.slice(0, 60) + "..." : safeArgs;
|
|
38
|
+
return `[${tool}: ${short}]`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
export function createDelegatedProgressWidget(
|
|
22
44
|
requestId: string,
|
|
23
45
|
agent: string,
|
|
@@ -25,11 +47,18 @@ export function createDelegatedProgressWidget(
|
|
|
25
47
|
task: string,
|
|
26
48
|
tasks: DelegatedSubagentTask[] | undefined,
|
|
27
49
|
theme: Theme,
|
|
50
|
+
model?: string,
|
|
28
51
|
): Container & { dispose?(): void } {
|
|
29
52
|
const contextSuffix = context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
30
|
-
const taskPreview = task.length >
|
|
53
|
+
const taskPreview = task.length > 200 ? `${task.slice(0, 200)}...` : task;
|
|
31
54
|
const parallelTasks = tasks ?? [];
|
|
32
55
|
const isParallel = parallelTasks.length > 0;
|
|
56
|
+
const parallelModels = [...new Set(parallelTasks
|
|
57
|
+
.map((task) => normalizeModelLabel(task.model))
|
|
58
|
+
.filter((entry): entry is string => !!entry))];
|
|
59
|
+
const requestModel = isParallel
|
|
60
|
+
? (parallelModels.length === 1 ? parallelModels[0] : undefined)
|
|
61
|
+
: normalizeModelLabel(model);
|
|
33
62
|
|
|
34
63
|
const container = new Container();
|
|
35
64
|
container.addChild(new Spacer(1));
|
|
@@ -44,7 +73,7 @@ export function createDelegatedProgressWidget(
|
|
|
44
73
|
const key = stateKey(state, elapsed);
|
|
45
74
|
if (key !== lastKey) {
|
|
46
75
|
lastKey = key;
|
|
47
|
-
rebuildBox(box, agent, contextSuffix, taskPreview, parallelTasks, isParallel, state, elapsed, theme);
|
|
76
|
+
rebuildBox(box, agent, contextSuffix, taskPreview, parallelTasks, isParallel, state, elapsed, theme, requestModel);
|
|
48
77
|
}
|
|
49
78
|
return Container.prototype.render.call(container, width);
|
|
50
79
|
};
|
|
@@ -55,11 +84,16 @@ export function createDelegatedProgressWidget(
|
|
|
55
84
|
function stateKey(state: DelegatedSubagentLiveState | undefined, elapsed: number): string {
|
|
56
85
|
if (!state) return "none";
|
|
57
86
|
const elapsedBucket = Math.floor(elapsed / 1000);
|
|
58
|
-
const tool = state.currentTool ??
|
|
87
|
+
const tool = state.currentTool ?? "";
|
|
88
|
+
const outputLen = state.recentOutput.length;
|
|
89
|
+
const outputTail = state.recentOutput.length > 0
|
|
90
|
+
? state.recentOutput[state.recentOutput.length - 1]?.slice(0, 80) ?? ""
|
|
91
|
+
: "";
|
|
92
|
+
const toolsLen = state.recentTools.length;
|
|
59
93
|
const taskProgressKey = state.taskProgress
|
|
60
94
|
.map((entry) => `${entry.index ?? ""}:${entry.agent}:${entry.status ?? ""}:${entry.currentTool ?? ""}:${entry.toolCount ?? 0}`)
|
|
61
95
|
.join("|");
|
|
62
|
-
return `${state.status}|${tool}|${state.toolCount}|${state.tokens}|${state.
|
|
96
|
+
return `${state.status}|${tool}|${state.toolCount}|${state.tokens}|${outputLen}:${outputTail}|${toolsLen}|${state.model ?? ""}|${taskProgressKey}|${elapsedBucket}`;
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
function rebuildBox(
|
|
@@ -72,34 +106,49 @@ function rebuildBox(
|
|
|
72
106
|
state: DelegatedSubagentLiveState | undefined,
|
|
73
107
|
elapsed: number,
|
|
74
108
|
theme: Theme,
|
|
109
|
+
requestModel?: string,
|
|
75
110
|
): void {
|
|
76
111
|
box.clear();
|
|
77
112
|
|
|
78
|
-
const
|
|
79
|
-
const
|
|
113
|
+
const taskProgress = state?.taskProgress ?? [];
|
|
114
|
+
const baseToolCount = state?.toolCount ?? 0;
|
|
115
|
+
const baseTokens = state?.tokens ?? 0;
|
|
116
|
+
const parallelToolCount = taskProgress.reduce((sum, entry) => sum + (entry.toolCount ?? 0), 0);
|
|
117
|
+
const parallelTokens = taskProgress.reduce((sum, entry) => sum + (entry.tokens ?? 0), 0);
|
|
118
|
+
const toolCount = isParallel && parallelToolCount > 0 ? parallelToolCount : baseToolCount;
|
|
119
|
+
const tokens = isParallel && parallelTokens > 0 ? parallelTokens : baseTokens;
|
|
120
|
+
const tokensLabel = formatTokens(tokens);
|
|
80
121
|
const duration = formatDuration(elapsed);
|
|
81
|
-
const isThinking = toolCount === 0 &&
|
|
122
|
+
const isThinking = toolCount === 0 && tokens === 0;
|
|
82
123
|
const icon = theme.fg("warning", "...");
|
|
124
|
+
const modelLabel = isParallel
|
|
125
|
+
? requestModel
|
|
126
|
+
: normalizeModelLabel(state?.model ?? requestModel);
|
|
127
|
+
const modelSuffix = modelLabel ? ` ${theme.fg("dim", modelLabel)}` : "";
|
|
83
128
|
const stats = isThinking
|
|
84
129
|
? `thinking, ${duration}`
|
|
85
|
-
: `${toolCount} tool${toolCount === 1 ? "" : "s"}, ${
|
|
86
|
-
const taskProgress = state?.taskProgress ?? [];
|
|
130
|
+
: `${toolCount} tool${toolCount === 1 ? "" : "s"}, ${tokensLabel} tok, ${duration}`;
|
|
87
131
|
|
|
132
|
+
// Header
|
|
88
133
|
if (isParallel) {
|
|
89
134
|
const completedCount = taskProgress.filter((entry) => entry.status === "completed").length;
|
|
90
135
|
const runningLabel = `parallel ${completedCount}/${parallelTasks.length} running`;
|
|
91
|
-
box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(runningLabel))}${contextSuffix} | ${stats}`, 0, 0));
|
|
136
|
+
box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(runningLabel))}${contextSuffix}${modelSuffix} | ${stats}`, 0, 0));
|
|
92
137
|
} else {
|
|
93
138
|
box.addChild(new Text(
|
|
94
|
-
`${icon} ${theme.fg("toolTitle", theme.bold(agent))}${contextSuffix} | ${stats}`,
|
|
139
|
+
`${icon} ${theme.fg("toolTitle", theme.bold(agent))}${contextSuffix}${modelSuffix} | ${stats}`,
|
|
95
140
|
0, 0,
|
|
96
141
|
));
|
|
97
142
|
}
|
|
98
143
|
box.addChild(new Spacer(1));
|
|
144
|
+
|
|
145
|
+
// Task preview
|
|
99
146
|
if (!isParallel) {
|
|
100
147
|
box.addChild(new Text(theme.fg("dim", `Task: ${taskPreview}`), 0, 0));
|
|
148
|
+
box.addChild(new Spacer(1));
|
|
101
149
|
}
|
|
102
150
|
|
|
151
|
+
// Parallel task list
|
|
103
152
|
if (isParallel) {
|
|
104
153
|
for (let index = 0; index < parallelTasks.length; index++) {
|
|
105
154
|
const task = parallelTasks[index]!;
|
|
@@ -111,35 +160,38 @@ function rebuildBox(
|
|
|
111
160
|
if (taskStatus === "running") {
|
|
112
161
|
const runningTool = progress.currentTool ? ` ${progress.currentTool}...` : "";
|
|
113
162
|
box.addChild(new Text(theme.fg("dim", ` ${task.agent}: running${runningTool}`), 0, 0));
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (taskStatus === "completed") {
|
|
163
|
+
} else if (taskStatus === "completed") {
|
|
117
164
|
const toolSuffix =
|
|
118
165
|
progress?.toolCount !== undefined
|
|
119
166
|
? ` (${progress.toolCount} tool${progress.toolCount === 1 ? "" : "s"})`
|
|
120
167
|
: "";
|
|
121
168
|
box.addChild(new Text(theme.fg("dim", ` ${task.agent}: completed${toolSuffix}`), 0, 0));
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (taskStatus === "failed") {
|
|
169
|
+
} else if (taskStatus === "failed") {
|
|
125
170
|
box.addChild(new Text(theme.fg("dim", ` ${task.agent}: failed`), 0, 0));
|
|
126
|
-
|
|
171
|
+
} else {
|
|
172
|
+
box.addChild(new Text(theme.fg("dim", ` ${task.agent}: pending`), 0, 0));
|
|
127
173
|
}
|
|
128
|
-
box.addChild(new Text(theme.fg("dim", ` ${task.agent}: pending`), 0, 0));
|
|
129
174
|
}
|
|
130
175
|
return;
|
|
131
176
|
}
|
|
132
177
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
// Unified tool stream: completed tools (dim) then active tool (warning) at bottom.
|
|
179
|
+
// When a tool finishes it moves from active → completed in place — no visual jump.
|
|
180
|
+
const recentTools = state?.recentTools ?? [];
|
|
181
|
+
for (const tool of recentTools) {
|
|
182
|
+
box.addChild(new Text(theme.fg("dim", formatToolCall(tool.tool, tool.args)), 0, 0));
|
|
183
|
+
}
|
|
184
|
+
if (state?.currentTool) {
|
|
185
|
+
const active = formatToolCall(state.currentTool, state.currentToolArgs ?? "");
|
|
186
|
+
box.addChild(new Text(theme.fg("warning", `> ${active}`), 0, 0));
|
|
139
187
|
}
|
|
140
188
|
|
|
189
|
+
// Recent output
|
|
141
190
|
if (state && state.recentOutput.length > 0) {
|
|
142
|
-
|
|
191
|
+
if (recentTools.length > 0 || state.currentTool) {
|
|
192
|
+
box.addChild(new Spacer(1));
|
|
193
|
+
}
|
|
194
|
+
for (const line of state.recentOutput) {
|
|
143
195
|
box.addChild(new Text(theme.fg("dim", ` ${line}`), 0, 0));
|
|
144
196
|
}
|
|
145
197
|
}
|