pi-subagents 0.13.4 → 0.14.1
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 +23 -4
- package/README.md +35 -14
- package/agent-management.ts +15 -6
- package/agent-manager-detail.ts +13 -3
- package/agent-manager-edit.ts +75 -23
- package/agent-manager-list.ts +12 -5
- package/agent-manager.ts +199 -11
- package/agents.ts +315 -20
- package/artifacts.ts +11 -5
- package/async-execution.ts +92 -73
- package/chain-clarify.ts +49 -160
- package/chain-execution.ts +38 -76
- package/execution.ts +53 -48
- package/index.ts +1 -1
- package/install.mjs +3 -3
- package/model-fallback.ts +8 -2
- package/package.json +1 -1
- package/parallel-utils.ts +5 -5
- package/prompt-template-bridge.ts +19 -8
- package/render.ts +23 -50
- package/schemas.ts +1 -1
- package/settings.ts +6 -4
- package/single-output.ts +2 -2
- package/skills.ts +165 -75
- package/subagent-executor.ts +52 -18
- package/subagent-runner.ts +171 -54
- package/types.ts +65 -14
- package/utils.ts +52 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.14.1] - 2026-04-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Completed foreground subagent results now return compact payloads instead of inlining full raw message histories and per-result progress objects, preventing long tool-heavy sync runs from overwhelming the parent agent return path.
|
|
9
|
+
- Prompt-template delegation now rebuilds minimal assistant messages from compact foreground results when raw message arrays are intentionally omitted.
|
|
10
|
+
- UI/status wording now uses plain text labels instead of glyph-heavy markers across foreground rendering, parallel summaries, save-result receipts, installer output, agent manager views, clarify screens, and the corresponding README/CHANGELOG examples.
|
|
11
|
+
- Added a realistic foreground integration repro for issue `#80` and cleaned up the touched tests to remove the remaining blunt `as any` fixture casts.
|
|
12
|
+
|
|
13
|
+
## [0.14.0] - 2026-04-14
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Builtin agents can now be customized through settings-backed field overrides in `~/.pi/agent/settings.json` and `.pi/settings.json` under `subagents.agentOverrides`, with `/agents` exposing a create/edit override flow instead of forcing full-file copies for model/thinking/tool/prompt tweaks.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Shared temp paths are now scoped under a user-specific temp root across async result storage, async run state, chain directories, artifact fallback storage, and detached async config files, avoiding cross-user collisions on shared machines while still handling arbitrary-UID/container environments where `os.userInfo()` can throw.
|
|
20
|
+
- Async/background runs now launch child `pi` processes in JSON mode, stream child events into `events.jsonl` with step metadata while the run is active, keep `output-<n>.log` live with human-readable child output, and document that `subagent-log-<id>.md` is a completion artifact.
|
|
21
|
+
- Bare model IDs now prefer the active parent-session provider when that provider actually exposes the model, across sync, chain, parallel, async, and clarify flows. Ambiguous bare IDs still fall back to conservative resolution.
|
|
22
|
+
- Skill resolution now includes local package roots declared in project/user `settings.json -> packages`, checks the effective task `cwd` before the runtime cwd, and still falls back to the runtime cwd when a nested task inherits package-provided skills from the repo root.
|
|
23
|
+
|
|
5
24
|
## [0.13.4] - 2026-04-13
|
|
6
25
|
|
|
7
26
|
### Fixed
|
|
@@ -485,7 +504,7 @@
|
|
|
485
504
|
- Pre-selects current thinking level if already set
|
|
486
505
|
- **Model selector in chain TUI** - Press `[m]` to select a different model for any step
|
|
487
506
|
- Fuzzy search through all available models
|
|
488
|
-
- Shows current model with
|
|
507
|
+
- Shows the current model with a `current` badge
|
|
489
508
|
- Provider/model format (e.g., `anthropic/claude-haiku-4-5`)
|
|
490
509
|
- Override indicator (✎) when model differs from agent default
|
|
491
510
|
- **Model visibility in chain execution** - Shows which model each step is using
|
|
@@ -523,8 +542,8 @@
|
|
|
523
542
|
|
|
524
543
|
### Improved
|
|
525
544
|
- **Per-step progress indicators** - When progress is enabled, each step shows its role:
|
|
526
|
-
- Step 1:
|
|
527
|
-
- Step 2+:
|
|
545
|
+
- Step 1: `writes progress.md`
|
|
546
|
+
- Step 2+: `reads progress.md`
|
|
528
547
|
- Clear visualization of progress.md data flow through the chain
|
|
529
548
|
- **Comprehensive tool descriptions** - Better documentation of chain variables:
|
|
530
549
|
- Tool description now explains `{task}`, `{previous}`, `{chain_dir}` in detail
|
|
@@ -580,7 +599,7 @@
|
|
|
580
599
|
### Improved
|
|
581
600
|
- **Tool description now explicitly shows the three modes** (SINGLE, CHAIN, PARALLEL) with syntax - helps agents pick the right mode when user says "scout → planner"
|
|
582
601
|
- **Chain execution observability** - Now shows:
|
|
583
|
-
- Chain visualization with status
|
|
602
|
+
- Chain visualization with status labels: `done scout → running planner` (`done`, `running`, `pending`, `failed`) - sequential chains only
|
|
584
603
|
- Accurate step counter: "step 1/2" instead of misleading "1/1"
|
|
585
604
|
- Current tool and recent output for running step
|
|
586
605
|
|
package/README.md
CHANGED
|
@@ -44,7 +44,14 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
|
|
|
44
44
|
|
|
45
45
|
Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
|
|
46
46
|
|
|
47
|
-
**Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them.
|
|
47
|
+
**Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them.
|
|
48
|
+
|
|
49
|
+
You can also override selected builtin fields without copying the whole agent. Builtin overrides are stored in settings under `subagents.agentOverrides`:
|
|
50
|
+
|
|
51
|
+
- User scope: `~/.pi/agent/settings.json`
|
|
52
|
+
- Project scope: `.pi/settings.json`
|
|
53
|
+
|
|
54
|
+
Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides. In `/agents`, press `e` on a builtin to create or edit its override. Overridden builtins show badges like `[builtin+user]` or `[builtin+project]`.
|
|
48
55
|
|
|
49
56
|
> **Note:** The `researcher` agent uses `web_search`, `fetch_content`, and `get_search_content` tools which require the [pi-web-access](https://github.com/nicobailon/pi-web-access) extension. Install it with `pi install npm:pi-web-access`.
|
|
50
57
|
|
|
@@ -74,7 +81,7 @@ The `thinking` field sets a default extended thinking level for the agent. At ru
|
|
|
74
81
|
|
|
75
82
|
`fallbackModels` is an optional ordered list of backup models to try when the primary model fails with a provider/model-style error such as quota, auth, timeout, or provider/model unavailable. In markdown frontmatter, declare it as a comma-separated string. In management `config` objects, you can pass either a comma-separated string or a string array.
|
|
76
83
|
|
|
77
|
-
Fallback resolution follows the same conservative model lookup as normal execution. Explicit `provider/model` values are used as-is. Bare model IDs
|
|
84
|
+
Fallback resolution follows the same conservative model lookup as normal execution. Explicit `provider/model` values are used as-is. Bare model IDs first prefer the current session provider when that provider actually exposes the model, then fall back to a unique registry match. If a bare ID is still ambiguous, it stays bare.
|
|
78
85
|
|
|
79
86
|
Fallback is only used for provider/model availability failures. Ordinary task failures such as bad `bash` commands, missing files, or other tool/runtime errors do not trigger a model hop.
|
|
80
87
|
|
|
@@ -221,7 +228,7 @@ Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay —
|
|
|
221
228
|
| Screen | Description |
|
|
222
229
|
|--------|-------------|
|
|
223
230
|
| List | Browse all agents and chains with search/filter, scope badges, chain badges |
|
|
224
|
-
| Detail | View resolved prompt, frontmatter fields, recent run history |
|
|
231
|
+
| Detail | View resolved prompt, frontmatter fields, recent run history, and active builtin override path |
|
|
225
232
|
| Edit | Edit fields with specialized pickers (model, thinking, skills, prompt editor) |
|
|
226
233
|
| Chain Detail | View chain steps with flow visualization and dependency map |
|
|
227
234
|
| Parallel Builder | Build parallel execution slots, add same agent multiple times, per-slot task overrides |
|
|
@@ -233,13 +240,15 @@ Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay —
|
|
|
233
240
|
- `Enter` — view detail
|
|
234
241
|
- Type any character — search/filter
|
|
235
242
|
- `Tab` — toggle selection (agents only)
|
|
236
|
-
- `
|
|
243
|
+
- `Alt+N` — new agent from template
|
|
237
244
|
- `Ctrl+K` — clone current item
|
|
238
245
|
- `Ctrl+D` or `Del` — delete current item
|
|
239
246
|
- `Ctrl+R` — run selected (1 agent: launch, 2+: sequential chain)
|
|
240
247
|
- `Ctrl+P` — open parallel builder (from selection or cursor agent)
|
|
241
248
|
- `Esc` — clear query, then selection, then close overlay
|
|
242
249
|
|
|
250
|
+
On a builtin detail screen, `e` opens the builtin override flow instead of cloning the whole agent. If no override exists yet, the manager asks whether to store it in user or project settings first.
|
|
251
|
+
|
|
243
252
|
**Parallel builder keybindings:**
|
|
244
253
|
- `↑↓` — navigate slots
|
|
245
254
|
- `Ctrl+A` — add agent (opens search picker)
|
|
@@ -253,6 +262,12 @@ Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay —
|
|
|
253
262
|
- `Tab` — toggle skip-clarify (defaults to on for all manager launches)
|
|
254
263
|
- `Esc` — back
|
|
255
264
|
|
|
265
|
+
**Builtin override edit keybindings:**
|
|
266
|
+
- `Ctrl+S` — save override
|
|
267
|
+
- `r` — reset the focused field back to the builtin value
|
|
268
|
+
- `D` — remove the entire override
|
|
269
|
+
- `Esc` — back
|
|
270
|
+
|
|
256
271
|
**Multi-select workflow:** Select agents with `Tab`, then press `Ctrl+R` for a sequential chain or `Ctrl+P` to open the parallel builder. The parallel builder lets you add the same agent multiple times, set per-slot task overrides, and launch N agents in parallel. Slots without a custom task use the shared task entered on the next screen.
|
|
257
272
|
|
|
258
273
|
## Chain Files
|
|
@@ -306,7 +321,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
306
321
|
- **Worktree Isolation**: `worktree: true` gives each parallel agent its own git worktree, preventing filesystem conflicts during concurrent execution
|
|
307
322
|
- **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
|
|
308
323
|
- **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`) plus optional recursion limits via `maxSubagentDepth`
|
|
309
|
-
- **Chain Artifacts**: Shared directory at `<tmpdir>/pi-chain-runs/{runId}/` for inter-step files
|
|
324
|
+
- **Chain Artifacts**: Shared directory at a user-scoped temp path like `<tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/` for inter-step files
|
|
310
325
|
- **Solo Agent Output**: Agents with `output` write to temp dir and return path to caller
|
|
311
326
|
- **Live Progress Display**: Real-time visibility during sync execution showing current tool, recent output, tokens, and duration
|
|
312
327
|
- **Output Truncation**: Configurable byte/line limits via `maxOutput`
|
|
@@ -397,9 +412,12 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
397
412
|
**Skill locations (project-first precedence):**
|
|
398
413
|
- Project: `.pi/skills/{name}/SKILL.md`
|
|
399
414
|
- Project packages: `.pi/npm/node_modules/*` via `package.json -> pi.skills`
|
|
415
|
+
- Project settings packages: local package roots from `.pi/settings.json -> packages`, then `package.json -> pi.skills`
|
|
416
|
+
- Current task cwd package: `<cwd>/package.json -> pi.skills`
|
|
400
417
|
- Project settings: `.pi/settings.json -> skills`
|
|
401
418
|
- User: `~/.pi/agent/skills/{name}/SKILL.md`
|
|
402
419
|
- User packages: `~/.pi/agent/npm/node_modules/*` via `package.json -> pi.skills`
|
|
420
|
+
- User settings packages: local package roots from `~/.pi/agent/settings.json -> packages`, then `package.json -> pi.skills`
|
|
403
421
|
- User settings: `~/.pi/agent/settings.json -> skills`
|
|
404
422
|
|
|
405
423
|
**Usage:**
|
|
@@ -529,7 +547,7 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
529
547
|
```typescript
|
|
530
548
|
{ action: "list" } // active async runs only
|
|
531
549
|
{ id: "a53ebe46" } // inspect one run
|
|
532
|
-
{ dir: "<tmpdir>/pi-async-subagent-runs/a53ebe46-..." }
|
|
550
|
+
{ dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/a53ebe46-..." }
|
|
533
551
|
```
|
|
534
552
|
|
|
535
553
|
**/subagents-status slash command:**
|
|
@@ -615,7 +633,7 @@ Notes:
|
|
|
615
633
|
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
|
|
616
634
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
617
635
|
| `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
|
|
618
|
-
| `chainDir` | string | `<tmpdir>/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
636
|
+
| `chainDir` | string | user-scoped temp dir like `<tmpdir>/pi-subagents-<scope>/chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
619
637
|
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
620
638
|
| `agentScope` | `"user" \| "project" \| "both"` | `both` | Agent discovery scope (project wins on name collisions) |
|
|
621
639
|
| `async` | boolean | false | Background execution (requires `clarify: false` for chains) |
|
|
@@ -850,7 +868,7 @@ Optional timeout (milliseconds) for each worktree hook invocation.
|
|
|
850
868
|
Default: `30000` ms.
|
|
851
869
|
|
|
852
870
|
## Chain Directory
|
|
853
|
-
Each chain run creates `<tmpdir>/pi-chain-runs/{runId}/` containing:
|
|
871
|
+
Each chain run creates a user-scoped temp directory like `<tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/` containing:
|
|
854
872
|
- `context.md` - Scout/context-builder output
|
|
855
873
|
- `plan.md` - Planner output
|
|
856
874
|
- `progress.md` - Worker/reviewer shared progress
|
|
@@ -863,7 +881,7 @@ Directories older than 24 hours are cleaned up on extension startup.
|
|
|
863
881
|
|
|
864
882
|
## Artifacts
|
|
865
883
|
|
|
866
|
-
Location: `{sessionDir}/subagent-artifacts/` or `<tmpdir>/pi-
|
|
884
|
+
Location: `{sessionDir}/subagent-artifacts/` or a user-scoped temp directory like `<tmpdir>/pi-subagents-<scope>/artifacts/`
|
|
867
885
|
|
|
868
886
|
Files per task:
|
|
869
887
|
- `{runId}_{agent}_input.md` - Task prompt
|
|
@@ -903,8 +921,8 @@ Requirements:
|
|
|
903
921
|
During sync execution, the collapsed view shows real-time progress for single, chain, and parallel modes.
|
|
904
922
|
|
|
905
923
|
**Chains:**
|
|
906
|
-
- Header:
|
|
907
|
-
- Chain visualization with status:
|
|
924
|
+
- Header: `running chain 1/2 | 8 tools, 1.4k tok, 38s`
|
|
925
|
+
- Chain visualization with status: `done scout → running planner` (`done`, `running`, `pending`, `failed`)
|
|
908
926
|
- Current tool: `> read: packages/tui/src/...`
|
|
909
927
|
- Recent output lines (last 2-3 lines)
|
|
910
928
|
|
|
@@ -915,7 +933,7 @@ During sync execution, the collapsed view shows real-time progress for single, c
|
|
|
915
933
|
|
|
916
934
|
Press **Ctrl+O** to expand the full streaming view with complete output per step.
|
|
917
935
|
|
|
918
|
-
> **Note:** Chain visualization (the
|
|
936
|
+
> **Note:** Chain visualization (the `done scout → running planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
|
|
919
937
|
|
|
920
938
|
## Nested subagent recursion guard
|
|
921
939
|
|
|
@@ -946,14 +964,17 @@ export PI_SUBAGENT_MAX_DEPTH=0 # disable the subagent tool entirely
|
|
|
946
964
|
Async runs write a dedicated observability folder:
|
|
947
965
|
|
|
948
966
|
```
|
|
949
|
-
<tmpdir>/pi-async-subagent-runs/<id>/
|
|
967
|
+
<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>/
|
|
950
968
|
status.json
|
|
951
969
|
events.jsonl
|
|
970
|
+
output-<n>.log
|
|
952
971
|
subagent-log-<id>.md
|
|
953
972
|
```
|
|
954
973
|
|
|
955
974
|
`status.json` is the source of truth for async progress and powers both the TUI widget and `/subagents-status`. Async status and result files are written atomically, so readers do not observe partial JSON during background updates.
|
|
956
975
|
|
|
976
|
+
`events.jsonl` is the live event stream. It includes high-level wrapper events plus child `pi` JSON events annotated with subagent metadata for the run and step they belong to. `output-<n>.log` is a live human-readable tail for the current step. `subagent-log-<id>.md` is written when the run completes.
|
|
977
|
+
|
|
957
978
|
When fallback is used in async/background mode, `status.json` and the final result JSON include the final selected model, ordered attempted models, and per-attempt outcomes so background runs are as debuggable as sync runs.
|
|
958
979
|
|
|
959
980
|
For programmatic access:
|
|
@@ -961,7 +982,7 @@ For programmatic access:
|
|
|
961
982
|
```typescript
|
|
962
983
|
subagent_status({ action: "list" })
|
|
963
984
|
subagent_status({ id: "<id>" })
|
|
964
|
-
subagent_status({ dir: "<tmpdir>/pi-async-subagent-runs/<id>" })
|
|
985
|
+
subagent_status({ dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>" })
|
|
965
986
|
```
|
|
966
987
|
|
|
967
988
|
For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed/failed runs. The overlay auto-refreshes every 2 seconds while it is open.
|
package/agent-management.ts
CHANGED
|
@@ -35,13 +35,18 @@ function parseCsv(value: string): string[] {
|
|
|
35
35
|
return [...new Set(value.split(",").map((v) => v.trim()).filter(Boolean))];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function configObject(config: unknown): Record<string, unknown
|
|
38
|
+
function configObject(config: unknown): { value?: Record<string, unknown>; error?: string } {
|
|
39
39
|
let val = config;
|
|
40
40
|
if (typeof val === "string") {
|
|
41
|
-
try {
|
|
41
|
+
try {
|
|
42
|
+
val = JSON.parse(val);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return { error: `config must be valid JSON: ${message}` };
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
|
-
if (!val || typeof val !== "object" || Array.isArray(val)) return
|
|
44
|
-
return val as Record<string, unknown
|
|
48
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) return {};
|
|
49
|
+
return { value: val as Record<string, unknown> };
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function hasKey(obj: Record<string, unknown>, key: string): boolean {
|
|
@@ -383,7 +388,9 @@ export function handleGet(params: ManagementParams, ctx: ManagementContext): Age
|
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
export function handleCreate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
386
|
-
const
|
|
391
|
+
const parsedConfig = configObject(params.config);
|
|
392
|
+
if (parsedConfig.error) return result(parsedConfig.error, true);
|
|
393
|
+
const cfg = parsedConfig.value;
|
|
387
394
|
if (!cfg) return result("config required for create.", true);
|
|
388
395
|
if (typeof cfg.name !== "string" || !cfg.name.trim()) return result("config.name is required and must be a non-empty string.", true);
|
|
389
396
|
if (typeof cfg.description !== "string" || !cfg.description.trim()) return result("config.description is required and must be a non-empty string.", true);
|
|
@@ -427,7 +434,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
427
434
|
export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
428
435
|
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for update.", true);
|
|
429
436
|
if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
|
|
430
|
-
const
|
|
437
|
+
const parsedConfig = configObject(params.config);
|
|
438
|
+
if (parsedConfig.error) return result(parsedConfig.error, true);
|
|
439
|
+
const cfg = parsedConfig.value;
|
|
431
440
|
if (!cfg) return result("config required for update.", true);
|
|
432
441
|
const warnings: string[] = [];
|
|
433
442
|
if (params.agent) {
|
package/agent-manager-detail.ts
CHANGED
|
@@ -61,6 +61,10 @@ function buildDetailLines(
|
|
|
61
61
|
const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
|
|
62
62
|
|
|
63
63
|
lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
|
|
64
|
+
if (agent.override) {
|
|
65
|
+
const overrideLabel = `${agent.override.scope} · ${formatPath(agent.override.path)}`;
|
|
66
|
+
lines.push(renderFieldLine("Override:", overrideLabel, contentWidth, theme));
|
|
67
|
+
}
|
|
64
68
|
lines.push(renderFieldLine("Thinking:", agent.thinking ?? "off", contentWidth, theme));
|
|
65
69
|
lines.push(renderFieldLine("Tools:", tools, contentWidth, theme));
|
|
66
70
|
lines.push(renderFieldLine("MCP:", mcp, contentWidth, theme));
|
|
@@ -102,7 +106,7 @@ function buildDetailLines(
|
|
|
102
106
|
|
|
103
107
|
for (const run of recentRuns) {
|
|
104
108
|
const when = pad(formatRelativeTime(run.ts), 8);
|
|
105
|
-
const status = run.status
|
|
109
|
+
const status = run.status;
|
|
106
110
|
const task = truncateToWidth(`"${run.task}"`, 34);
|
|
107
111
|
const tail = run.status === "ok" ? formatDuration(run.duration) : `exit ${run.exit ?? 1}`;
|
|
108
112
|
lines.push(truncateToWidth(` ${when} ${status} ${task} ${tail}`, contentWidth));
|
|
@@ -131,7 +135,11 @@ export function renderDetail(
|
|
|
131
135
|
theme: Theme,
|
|
132
136
|
): string[] {
|
|
133
137
|
const lines: string[] = [];
|
|
134
|
-
const scopeBadge = agent.source === "builtin"
|
|
138
|
+
const scopeBadge = agent.source === "builtin"
|
|
139
|
+
? (agent.override ? `[builtin+${agent.override.scope}]` : "[builtin]")
|
|
140
|
+
: agent.source === "project"
|
|
141
|
+
? "[proj]"
|
|
142
|
+
: "[user]";
|
|
135
143
|
const headerText = ` ${agent.name} ${scopeBadge} ${formatPath(agent.filePath)} `;
|
|
136
144
|
lines.push(renderHeader(headerText, width, theme));
|
|
137
145
|
lines.push(row("", width, theme));
|
|
@@ -152,7 +160,9 @@ export function renderDetail(
|
|
|
152
160
|
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
|
|
153
161
|
|
|
154
162
|
const footer = agent.source === "builtin"
|
|
155
|
-
?
|
|
163
|
+
? agent.override
|
|
164
|
+
? " [l]aunch [e]dit override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
165
|
+
: " [l]aunch [e]create override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
156
166
|
: " [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ";
|
|
157
167
|
lines.push(renderFooter(footer, width, theme));
|
|
158
168
|
return lines;
|
package/agent-manager-edit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { AgentConfig } from "./agents.ts";
|
|
3
|
+
import type { AgentConfig, BuiltinAgentOverrideBase } from "./agents.ts";
|
|
4
4
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
|
|
5
5
|
import type { TextEditorState } from "./text-editor.ts";
|
|
6
6
|
import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
|
|
@@ -8,30 +8,70 @@ import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render
|
|
|
8
8
|
export interface ModelInfo { provider: string; id: string; fullId: string; }
|
|
9
9
|
export interface SkillInfo { name: string; source: string; description?: string; }
|
|
10
10
|
export type EditScreen = "edit" | "edit-field" | "edit-prompt";
|
|
11
|
+
export type EditField = typeof FIELD_ORDER[number];
|
|
12
|
+
|
|
11
13
|
export interface EditState {
|
|
12
14
|
draft: AgentConfig; isNew: boolean; fieldIndex: number; fieldMode: "text" | "model" | "thinking" | "skills" | null;
|
|
13
15
|
fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; filteredModels: ModelInfo[];
|
|
14
16
|
thinkingCursor: number; skillSearchQuery: string; skillCursor: number; filteredSkills: SkillInfo[]; skillSelected: Set<string>; error?: string;
|
|
17
|
+
fields: EditField[];
|
|
18
|
+
title?: string;
|
|
19
|
+
overrideBase?: BuiltinAgentOverrideBase;
|
|
20
|
+
}
|
|
21
|
+
export interface EditInputResult { action?: "save" | "discard" | "delete"; nextScreen?: EditScreen; }
|
|
22
|
+
export interface CreateEditStateOptions {
|
|
23
|
+
fields?: EditField[];
|
|
24
|
+
title?: string;
|
|
25
|
+
overrideBase?: BuiltinAgentOverrideBase;
|
|
15
26
|
}
|
|
16
|
-
export interface EditInputResult { action?: "save" | "discard"; nextScreen?: EditScreen; }
|
|
17
27
|
|
|
18
28
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
19
29
|
const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
20
|
-
type EditField = typeof FIELD_ORDER[number];
|
|
21
30
|
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
22
31
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
23
32
|
const MODEL_SELECTOR_HEIGHT = 10;
|
|
24
33
|
const SKILL_SELECTOR_HEIGHT = 10;
|
|
25
34
|
|
|
26
|
-
function formatTools(draft: AgentConfig): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
|
|
35
|
+
function formatTools(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
|
|
36
|
+
function toolList(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string[] | undefined { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools : undefined; }
|
|
27
37
|
function parseTools(value: string): { tools?: string[]; mcp?: string[] } { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); const tools: string[] = []; const mcp: string[] = []; for (const item of items) { if (item.startsWith("mcp:")) { const name = item.slice(4).trim(); if (name) mcp.push(name); } else { tools.push(item); } } return { tools: tools.length > 0 ? tools : undefined, mcp: mcp.length > 0 ? mcp : undefined }; }
|
|
28
38
|
function parseCommaList(value: string): string[] | undefined { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); return items.length > 0 ? items : undefined; }
|
|
39
|
+
function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean { if (!a && !b) return true; if (!a || !b || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }
|
|
40
|
+
|
|
41
|
+
function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
|
|
42
|
+
const base = state.overrideBase;
|
|
43
|
+
if (!base) return false;
|
|
44
|
+
switch (field) {
|
|
45
|
+
case "model": return state.draft.model === base.model;
|
|
46
|
+
case "fallbackModels": return arraysEqual(state.draft.fallbackModels, base.fallbackModels);
|
|
47
|
+
case "thinking": return state.draft.thinking === base.thinking;
|
|
48
|
+
case "tools": return arraysEqual(toolList(state.draft), toolList(base));
|
|
49
|
+
case "skills": return arraysEqual(state.draft.skills, base.skills);
|
|
50
|
+
case "prompt": return state.draft.systemPrompt === base.systemPrompt;
|
|
51
|
+
default: return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
function resetFieldToBase(field: EditField, state: EditState): void {
|
|
56
|
+
const base = state.overrideBase;
|
|
57
|
+
if (!base) return;
|
|
58
|
+
switch (field) {
|
|
59
|
+
case "model": state.draft.model = base.model; break;
|
|
60
|
+
case "fallbackModels": state.draft.fallbackModels = base.fallbackModels ? [...base.fallbackModels] : undefined; break;
|
|
61
|
+
case "thinking": state.draft.thinking = base.thinking; break;
|
|
62
|
+
case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
|
|
63
|
+
case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
|
|
64
|
+
case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
|
|
65
|
+
default: break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[], options: CreateEditStateOptions = {}): EditState {
|
|
31
70
|
return {
|
|
32
71
|
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, fallbackModels: draft.fallbackModels ? [...draft.fallbackModels] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
33
72
|
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
34
73
|
modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
74
|
+
fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase,
|
|
35
75
|
};
|
|
36
76
|
}
|
|
37
77
|
|
|
@@ -71,15 +111,15 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
|
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
function openModelPicker(state: EditState, models: ModelInfo[]): void {
|
|
74
|
-
state.fieldIndex =
|
|
114
|
+
state.fieldIndex = state.fields.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
|
|
75
115
|
const idx = state.filteredModels.findIndex((m) => m.fullId === state.draft.model || m.id === state.draft.model); state.modelCursor = idx >= 0 ? idx : 0;
|
|
76
116
|
}
|
|
77
117
|
function openThinkingPicker(state: EditState): void {
|
|
78
|
-
state.fieldIndex =
|
|
118
|
+
state.fieldIndex = state.fields.indexOf("thinking"); state.fieldMode = "thinking";
|
|
79
119
|
const idx = THINKING_LEVELS.indexOf((state.draft.thinking ?? "off") as ThinkingLevel); state.thinkingCursor = idx >= 0 ? idx : 0;
|
|
80
120
|
}
|
|
81
121
|
function openSkillPicker(state: EditState, skills: SkillInfo[]): void {
|
|
82
|
-
state.fieldIndex =
|
|
122
|
+
state.fieldIndex = state.fields.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
function renderModelPicker(state: EditState, width: number, theme: Theme): string[] {
|
|
@@ -183,9 +223,11 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
183
223
|
if (screen === "edit") {
|
|
184
224
|
if (matchesKey(data, "ctrl+s")) return { action: "save" };
|
|
185
225
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { action: "discard" };
|
|
226
|
+
if (data === "D" && state.overrideBase) return { action: "delete" };
|
|
186
227
|
if (matchesKey(data, "up")) { state.fieldIndex = Math.max(0, state.fieldIndex - 1); return; }
|
|
187
|
-
if (matchesKey(data, "down")) { state.fieldIndex = Math.min(
|
|
188
|
-
const field =
|
|
228
|
+
if (matchesKey(data, "down")) { state.fieldIndex = Math.min(state.fields.length - 1, state.fieldIndex + 1); return; }
|
|
229
|
+
const field = state.fields[state.fieldIndex]!;
|
|
230
|
+
if (data === "r" && state.overrideBase) { resetFieldToBase(field, state); return; }
|
|
189
231
|
if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
|
|
190
232
|
if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
|
|
191
233
|
if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
|
|
@@ -234,7 +276,7 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
234
276
|
return;
|
|
235
277
|
}
|
|
236
278
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
237
|
-
if (matchesKey(data, "return")) { const field =
|
|
279
|
+
if (matchesKey(data, "return")) { const field = state.fields[state.fieldIndex]!; applyFieldValue(field, state, state.fieldEditor.buffer); state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
238
280
|
if (matchesKey(data, "tab")) return;
|
|
239
281
|
const innerW = width - 2; const labelWidth = 12; const textWidth = Math.max(10, innerW - labelWidth - 6);
|
|
240
282
|
const nextState = handleEditorInput(state.fieldEditor, data, textWidth); if (nextState) state.fieldEditor = nextState; return;
|
|
@@ -256,13 +298,14 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
|
|
|
256
298
|
if (screen === "edit-prompt") return renderPromptEditor(state, width, theme);
|
|
257
299
|
const lines: string[] = [];
|
|
258
300
|
const scopeBadge = state.draft.source === "user" ? "[user]" : "[proj]"; const label = state.isNew ? " [new]" : "";
|
|
259
|
-
lines.push(renderHeader(` Editing: ${state.draft.name} ${scopeBadge}${label} `, width, theme));
|
|
301
|
+
lines.push(renderHeader(` ${state.title ?? `Editing: ${state.draft.name} ${scopeBadge}${label}`} `, width, theme));
|
|
260
302
|
lines.push(row("", width, theme));
|
|
261
303
|
const innerW = width - 2; const labelWidth = 12; const valueWidth = Math.max(10, innerW - labelWidth - 6);
|
|
262
|
-
for (let i = 0; i <
|
|
263
|
-
const field =
|
|
304
|
+
for (let i = 0; i < state.fields.length; i++) {
|
|
305
|
+
const field = state.fields[i]!; if (field === "prompt") break;
|
|
264
306
|
const isFocused = i === state.fieldIndex; const prefix = isFocused ? theme.fg("accent", "▸ ") : " ";
|
|
265
|
-
const
|
|
307
|
+
const rawLabel = pad(`${field[0]!.toUpperCase()}${field.slice(1)}:`, labelWidth);
|
|
308
|
+
const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
|
|
266
309
|
if (field === "progress") { const toggle = state.draft.defaultProgress ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.defaultProgress ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
267
310
|
if (field === "interactive") { const toggle = state.draft.interactive ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.interactive ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
268
311
|
let displayValue = truncateToWidth(valueText, valueWidth);
|
|
@@ -275,14 +318,23 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
|
|
|
275
318
|
}
|
|
276
319
|
lines.push(row(` ${prefix}${labelText} [${displayValue}]`, width, theme));
|
|
277
320
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
321
|
+
if (state.fields.includes("prompt")) {
|
|
322
|
+
lines.push(row("", width, theme));
|
|
323
|
+
const promptIndex = state.fields.indexOf("prompt");
|
|
324
|
+
const promptFocused = state.fieldIndex === promptIndex;
|
|
325
|
+
const promptPrefix = promptFocused ? theme.fg("accent", "▸ ") : " ";
|
|
326
|
+
const promptTitle = state.overrideBase && !fieldValueMatchesBase("prompt", state)
|
|
327
|
+
? theme.fg("accent", "── System Prompt ──")
|
|
328
|
+
: theme.fg("dim", "── System Prompt ──");
|
|
329
|
+
lines.push(row(` ${promptPrefix}${promptTitle}`, width, theme));
|
|
330
|
+
const previewWidth = innerW - 2; const wrapped = wrapText(state.draft.systemPrompt ?? "", previewWidth); const previewLines = wrapped.lines.slice(0, 4);
|
|
331
|
+
for (const line of previewLines) lines.push(row(` ${line}`, width, theme));
|
|
332
|
+
for (let i = previewLines.length; i < 4; i++) lines.push(row("", width, theme));
|
|
333
|
+
}
|
|
285
334
|
if (state.error) lines.push(row(` ${theme.fg("error", state.error)}`, width, theme)); else lines.push(row("", width, theme));
|
|
286
|
-
|
|
335
|
+
const footer = state.overrideBase
|
|
336
|
+
? " [ctrl+s] save [r] reset field [D] remove override [esc] back "
|
|
337
|
+
: " [ctrl+s] save [esc] back ";
|
|
338
|
+
lines.push(renderFooter(footer, width, theme));
|
|
287
339
|
return lines;
|
|
288
340
|
}
|
package/agent-manager-list.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface ListAgent {
|
|
|
9
9
|
description: string;
|
|
10
10
|
model?: string;
|
|
11
11
|
source: AgentSource;
|
|
12
|
+
overrideScope?: "user" | "project";
|
|
12
13
|
kind: "agent" | "chain";
|
|
13
14
|
stepCount?: number;
|
|
14
15
|
}
|
|
@@ -190,7 +191,7 @@ export function renderList(
|
|
|
190
191
|
const innerW = width - 2;
|
|
191
192
|
const nameWidth = 16;
|
|
192
193
|
const modelWidth = 12;
|
|
193
|
-
const scopeWidth =
|
|
194
|
+
const scopeWidth = 17;
|
|
194
195
|
|
|
195
196
|
for (let i = 0; i < visible.length; i++) {
|
|
196
197
|
const agent = visible[i]!;
|
|
@@ -199,16 +200,22 @@ export function renderList(
|
|
|
199
200
|
const count = selectionCount(state.selected, agent.id);
|
|
200
201
|
const isShadowed = agent.kind === "agent" && agent.source === "project" && userNames.has(agent.name);
|
|
201
202
|
|
|
202
|
-
const cursorChar = isCursor ? theme.fg("accent", "
|
|
203
|
-
const selectBadge = count >
|
|
204
|
-
const shadowMarker = isShadowed ? theme.fg("warning", "
|
|
203
|
+
const cursorChar = isCursor ? theme.fg("accent", ">") : " ";
|
|
204
|
+
const selectBadge = count > 0 ? theme.fg("accent", String(count).padStart(2)) : " ";
|
|
205
|
+
const shadowMarker = isShadowed ? theme.fg("warning", "!") : " ";
|
|
205
206
|
const prefix = `${cursorChar}${selectBadge}${shadowMarker} `;
|
|
206
207
|
|
|
207
208
|
const modelRaw = agent.kind === "chain" ? `${agent.stepCount ?? 0} steps` : (agent.model ?? "default");
|
|
208
209
|
const modelDisplay = modelRaw.includes("/") ? modelRaw.split("/").pop() ?? modelRaw : modelRaw;
|
|
209
210
|
const nameText = isCursor ? theme.fg("accent", agent.name) : agent.name;
|
|
210
211
|
const modelText = theme.fg("dim", modelDisplay);
|
|
211
|
-
const scopeLabel = agent.kind === "chain"
|
|
212
|
+
const scopeLabel = agent.kind === "chain"
|
|
213
|
+
? "[chain]"
|
|
214
|
+
: agent.source === "builtin"
|
|
215
|
+
? (agent.overrideScope ? `[builtin+${agent.overrideScope}]` : "[builtin]")
|
|
216
|
+
: agent.source === "project"
|
|
217
|
+
? "[proj]"
|
|
218
|
+
: "[user]";
|
|
212
219
|
const scopeBadge = theme.fg("dim", scopeLabel);
|
|
213
220
|
const descText = theme.fg("dim", agent.description);
|
|
214
221
|
|