pi-jar 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -18
- package/docs/WORKFLOWS.md +32 -10
- package/extensions/index.ts +74 -13
- package/package.json +1 -1
- package/src/activity-view.ts +12 -5
- package/src/ask-tool.ts +12 -7
- package/src/delegate-worktree.ts +360 -0
- package/src/delegate.ts +826 -124
- package/src/democracy.ts +192 -0
- package/src/discussion.ts +153 -0
- package/src/footer.ts +4 -3
- package/src/goal-loop.ts +8 -1
- package/src/model-roles.ts +58 -29
- package/src/roles-ui.ts +18 -10
- package/src/settings-ui.ts +41 -15
- package/src/settings.ts +9 -1
- package/src/status.ts +2 -1
- package/src/task-tool.ts +33 -18
- package/src/tasks.ts +7 -7
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ The showcase is captured from a real pi-jar terminal session: the torch-style `
|
|
|
18
18
|
| **Composer** | Rounded input that grows with your draft (up to ~60% of the terminal), click-to-place cursor in fullscreen, dim **next-prompt suggestions** you accept with <kbd>Tab</kbd>, and **Ember** — a tiny flame mascot that blinks, cheers, focuses, dozes and reacts. |
|
|
19
19
|
| **Plan mode** | Read-only exploration; the agent must write a structured plan file and submit it. You review it in a split view (headings on the left, section on the right) and approve, compact-and-approve, refine or stop. |
|
|
20
20
|
| **Goal mode** | Set an outcome; the agent must break it into tracked tasks and keeps working until they are done, then an **auditor** pass verifies the goal before it can be marked complete. |
|
|
21
|
-
| **Roles** | Named model roles (`default`, `smol`, `slow`, `plan`, `implement`, `advisor`, `task`, `commit`, plus your own) with
|
|
21
|
+
| **Roles** | Named model roles (`default`, `smol`, `slow`, `plan`, `implement`, `advisor`, `moderator`, `scout`, `worker`, `reviewer`, `task`, `commit`, plus your own) with aliases, effort, per-role fallback models and project overrides. |
|
|
22
22
|
| **Advisor** | A second-opinion model: the agent calls `jar_advisor` for risky decisions or when stuck, `/advisor [focus]` asks on demand, and automatic gates consult it when the agent repeats the same tool call or keeps failing. |
|
|
23
23
|
| **Usage & context** | `/usage` shows session cost and tokens per model (advisor and commit calls included) and plan-limit bars with reset times; `/context` draws a grid of what fills the context window, with a per-file, per-skill and per-tool breakdown. |
|
|
24
24
|
| **Commit** | `/jar commit [note]` drafts a message for the staged changes with the `commit` role, lets you edit it, then commits (never pushes). |
|
|
25
25
|
| **Tasks & questions** | A Claude/omp-style `jar_todo` checklist with subtasks and per-task progress that the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
|
|
26
26
|
| **Change review** | Every file the agent or its editing subagents change is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files (each counted once) next to a colored diff, with accept or revert per file or all at once. |
|
|
27
27
|
| **Background shells** | `jar_shell` runs dev servers, watchers and long tests in the background; the agent is woken when a watch pattern matches or the process exits, so it never polls. Running shells get a footer row; click it (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>) to tail or kill them. |
|
|
28
|
-
| **Subagents** |
|
|
28
|
+
| **Subagents** | The main agent becomes a moderator over a configurable retained pool (2, 4, 6, 8 or 16; default 4). `jar_delegate` spawns `scout`, `fork` or sandboxed `worktree` agents; `jar_subagent` peeks, steers, asks BTW questions, pauses, resumes and stops them. Workers can exchange bounded structured Q/A through `jar_discuss`; worktree changes stay isolated until stop, then safely enter `/diff`. |
|
|
29
29
|
| **Sessions & history** | Recent sessions (with their goal and plan) on the welcome card, one click from resuming; <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd> searches earlier prompts; pasted images show as chips under the composer. |
|
|
30
30
|
| **Footer & themes** | Responsive footer (model, effort, session, cwd, context, RAM, cost, quota, goal, roles, branch, live subagents and shells) with unicode, [Nerd Font](https://www.nerdfonts.com) or ascii icons, and seven dark themes. |
|
|
31
31
|
|
|
@@ -175,10 +175,15 @@ Roles map a purpose to a model. They live in `~/.pi/agent/pi-jar-roles.json` (Pi
|
|
|
175
175
|
"plan": "@slow",
|
|
176
176
|
"advisor": "@slow:xhigh",
|
|
177
177
|
"smol": "anthropic/claude-haiku-4-5-20251001",
|
|
178
|
-
"
|
|
178
|
+
"scout": "@smol:minimal",
|
|
179
|
+
"worker": "anthropic/claude-sonnet-5:medium",
|
|
180
|
+
"reviewer": "@default:medium"
|
|
179
181
|
},
|
|
180
|
-
"
|
|
181
|
-
|
|
182
|
+
"fallbacks": {
|
|
183
|
+
"scout": ["openai/gpt-5-mini:low"],
|
|
184
|
+
"worker": ["@default"]
|
|
185
|
+
},
|
|
186
|
+
"cycleOrder": ["smol", "default", "slow"]
|
|
182
187
|
}
|
|
183
188
|
```
|
|
184
189
|
|
|
@@ -190,13 +195,17 @@ Roles map a purpose to a model. They live in `~/.pi/agent/pi-jar-roles.json` (Pi
|
|
|
190
195
|
| `plan` | plan mode (restored when you leave it) |
|
|
191
196
|
| `implement` | executing an approved plan and goal implement rounds (falls back to the current model) |
|
|
192
197
|
| `advisor` | `jar_advisor`, `/advisor`, stuck-work gates and the goal audit |
|
|
193
|
-
| `
|
|
198
|
+
| `moderator` | optional model assignment for the coordinating main agent |
|
|
199
|
+
| `scout` | default for fresh read-only delegated discovery; ideal for a cheap model |
|
|
200
|
+
| `worker` | default for sandboxed worktree implementation |
|
|
201
|
+
| `reviewer` | default for context-forked read-only review |
|
|
202
|
+
| `task` | legacy/generic explicit subagent role |
|
|
194
203
|
| `commit` | `/jar commit` messages |
|
|
195
204
|
| `smol` / `slow` | cycling with <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>M</kbd> |
|
|
196
205
|
|
|
197
206
|
Any other valid name (`a-z`, `0-9`, `-`) is a custom role.
|
|
198
207
|
- **Switching is automatic and scoped.** Entering plan mode applies `plan`; approving applies your chosen role or `implement` for the run; goal rounds alternate `implement` and `advisor`. Each temporary switch restores the previous model afterwards — unless you picked a model or effort yourself in the meantime, which always wins.
|
|
199
|
-
- `/roles` opens a split manager: role list on the left; resolved model, alias chain, effort, scope and usage on the right. Keys: `m` model, `a` alias, `t` effort, `s` move between global/project scope, <kbd>Enter</kbd> activate, `c` clear, `n` new role, `d` delete custom role.
|
|
208
|
+
- `/roles` opens a split manager: role list on the left; resolved model, alias chain, effort, **fallback model**, scope and usage on the right. Keys: `m` primary model, `f` fallback model, `a` alias, `t` effort, `s` move between global/project scope, <kbd>Enter</kbd> activate, `c` clear, `n` new role, `d` delete custom role. The CLI still supports an ordered chain with `/roles fallback ROLE MODEL...`.
|
|
200
209
|
- Older v1 files are read and upgraded in memory; they are rewritten as v2 only when you change a role.
|
|
201
210
|
|
|
202
211
|
## Advisor
|
|
@@ -208,9 +217,9 @@ The advisor is a second model (the `advisor` role; the current model if unassign
|
|
|
208
217
|
- **Gates** — when the agent makes the same tool call three times within its last eight calls, the call is blocked and the advisor's review is returned instead; after three failing tool results in a row, the advice is steered into the running turn. At most two automatic consultations per prompt.
|
|
209
218
|
- Settings → Pi toggles the advisor and the gates. Advisor calls are counted in `/usage`. The footer and welcome show it as a working teammate while it thinks.
|
|
210
219
|
|
|
211
|
-
###
|
|
220
|
+
### Role fallback models
|
|
212
221
|
|
|
213
|
-
|
|
222
|
+
Every role can have an ordered fallback chain. The primary remains configured with `/roles set ROLE`:
|
|
214
223
|
|
|
215
224
|
```text
|
|
216
225
|
/roles fallback advisor openai/gpt-5:high @smol
|
|
@@ -224,7 +233,7 @@ Add `--project` when saving or clearing to override the global chain for this pr
|
|
|
224
233
|
"fallbacks": { "advisor": ["openai/gpt-5:high", "@smol"] }
|
|
225
234
|
```
|
|
226
235
|
|
|
227
|
-
|
|
236
|
+
Role activation and delegated model selection try the primary first and then configured fallbacks in order; aliases and per-model effort are supported and duplicate model/effort pairs are skipped. Side-model workflows such as advisor and commit keep their own retry/error reporting while using the same fallback configuration. An empty project fallback list disables inherited global fallbacks. This makes roles such as `scout` practical to pin to a cheap model with a more capable fallback instead of silently escalating every task.
|
|
228
237
|
|
|
229
238
|
This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-advisor/) currently resolves one configured advisor model rather than a fallback chain.
|
|
230
239
|
|
|
@@ -237,17 +246,21 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
237
246
|
|
|
238
247
|
## Tasks, questions and history
|
|
239
248
|
|
|
240
|
-
- **`jar_todo`** — a Claude/omp-style task list the agent keeps for
|
|
249
|
+
- **`jar_todo`** — a Claude/omp-style task list the agent keeps for multi-step work. A new request starts with `write`: the complete fresh plan, replacing old completed work. `update` changes one stable `id` without disturbing omitted fields/subtasks, and `remove` deletes one task tree. `append` is reserved mainly for new requirements introduced while an existing checklist is already active, such as user steering; if the previous list is fully completed, the agent writes a new list instead of extending the fossil record. IDs are returned for every task. Each task is `pending`, `in_progress` (exactly one leaf at a time) or `completed`, with optional `activeForm`. `start` and `done` deliberately return only the task they changed, while the internal details still carry the checklist state needed by the UI and parent/subagent mirroring. Tasks can have one level of **subtasks**; parent status and progress roll up automatically. The live checklist above the composer shows `✔`, `◼` and `☐`; `/jar tasks` remains the human editor.
|
|
241
250
|
```json
|
|
242
|
-
{ "action": "
|
|
243
|
-
{ "action": "
|
|
244
|
-
{ "action": "
|
|
251
|
+
{ "action": "write", "todos": [{ "content": "Implement feature", "status": "in_progress" }, { "content": "Run tests", "status": "pending" }] }
|
|
252
|
+
{ "action": "update", "id": "<returned-id>", "status": "completed" }
|
|
253
|
+
{ "action": "append", "todos": [{ "content": "Handle newly requested edge case", "status": "pending" }] }
|
|
245
254
|
```
|
|
246
|
-
Use `parent` with `append` to add
|
|
255
|
+
Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
|
|
247
256
|
- **`jar_ask`** — structured questions: numbered options with descriptions, single or multi-select, *Type your own answer* (multi-line, paste-friendly) and *Chat about this* to discuss before choosing.
|
|
248
257
|
- **`/diff`** — review what the agent (and its editing subagents) changed since the first edit to each file: files with `+/−` counts on the left, a numbered, colored diff on the right. `a` accept (keep, stop tracking), `r` then `y` revert (restore the original, or remove a file it created), `A`/`R` for all. The footer shows `± N files · /diff` while anything is unreviewed; each file counts once no matter who edited it. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
|
|
249
258
|
- **`jar_shell`** — `start` (with optional `name`, `watch` regex and `notify`), `list`, `output`, `kill`. Output is ANSI-stripped and bounded (2000 lines). When a watch matches or the process ends, a visible message wakes the agent (queued if it is busy). Each running shell gets a footer row; the activity view (`/jar shells`, a click on the row) tails it live (`x` kill, `f` follow). All shells stop when the session ends.
|
|
250
|
-
- **`jar_delegate`** — up to
|
|
259
|
+
- **`jar_delegate`** — spawns **retained session-scoped** subagents up to **Max subagents** in `/jar settings` (2, 4, 6, 8 or 16; default 4). Idle/paused children and live legacy workers count toward the shared limit, not the parent; over-cap batches are rejected before launch. Lowering the limit does not terminate existing children, but blocks new launches until there is room. The parent is given moderator context and should coordinate rather than duplicate their work. `mode: "scout"` is fresh/read-only and defaults to the `scout` role; `fork` is a true Pi session fork, read-only, and defaults to `reviewer`; `worktree` forks context and defaults to `worker`, editing inside a path-guarded disposable Git worktree. A settled retained agent becomes `idle`, keeping its process, model context and workspace for reuse. The old `write: true` direct shared-workspace mode remains one-shot for compatibility.
|
|
260
|
+
- **`jar_subagent`** — moderator control plane: `peek` returns high-level task/checklist progress, current activity, changed files, remaining work, workspace and last report; `steer` redirects a running worker; `ask` injects a terse BTW question and waits for its answer; `pause` aborts the current operation but keeps context/workspace; `resume` wakes that same agent with an optional instruction; `stop` retires it and returns a structured handoff with progress, work directory, changed/applied files and what remains.
|
|
261
|
+
- **Worktree finalization** — worktree edits do **not** touch the parent merely because a worker becomes idle. They remain private across pause/resume cycles. On `jar_subagent stop`, pi-jar verifies parent drift and reviewability, applies safe changes, and adds them to `/diff`; conflicts remain isolated and the workspace path is reported. Resolve parent drift and retry `stop` to reconcile; failed workspaces are retained rather than trimmed out of report history. Process closure is awaited before finalization. All replacements/rollback backups are prepared before parent mutation; failed multi-file application rolls back rather than silently leaving a partial result.
|
|
262
|
+
- **`jar_democracy`** — **exceptional only**, for a persistent **super-complex** issue with several viable options and evidence of at least two distinct failed approaches. The moderator opens 2–8 evidence-backed options, resumes explicitly relevant idle/paused **fresh-context read-only scouts** or spawns fresh scouts within the same pool limit, and collects one private ballot per voter before publishing the tally. A **strict majority of the invited electorate** wins; ties, plurality without majority, and missing/invalid ballots require the user to choose among leading options, with evidence/tradeoffs rather than invented probabilities. TUI uses the structured question picker; RPC returns `needs-user` for the moderator to resolve with `jar_ask`. Voting returns a recommendation and **never implements it** or bypasses safety/approval. Prefer new scouts if past discussions could bias reused agents. Do not use democracy for ordinary complexity or routine decisions.
|
|
263
|
+
- **`jar_discuss`** — a bounded shared discussion paper for virtual agent-to-agent discussion. Questions and answers have stable IDs plus author/target metadata; entries are capped at 1600 characters, the newest 64 are retained, and serialized UTF-8 is capped at 64 KiB (older entries are evicted until it fits). Agents use it for narrow Q/A, not as another transcript.
|
|
251
264
|
- **Activity view** — one split view (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>, `/jar activity`, or a click on a footer row) for subagents, background shells and other extensions' teammates: list on the left, live details on the right (auto-follows; scroll up to pause, `f` to follow again). For a subagent the details show its task, its own checklist with subtasks, and a transcript like omp's: each tool call and message is one collapsed line that expands on demand (`Tab` into it, `↑↓`, `Enter`, or click) to show arguments and output, while streaming text shows live. `s` opens a steering input: type a message and press `Enter` to redirect the running subagent. `x` stops just that one. Finished runs stay listed for review.
|
|
252
265
|
- **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
|
|
253
266
|
- Pi's built-in read/shell/edit/write tool cards render compactly; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away.
|
|
@@ -258,7 +271,7 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
258
271
|
|
|
259
272
|
- **Appearance** — accent, motion, rounded composer, Ember mascot, next-prompt suggestions, pi-jar UI, icons (`unicode` default, `nerd` for a [Nerd Font](https://www.nerdfonts.com) terminal like omp's nerd preset, `ascii` for plain labels).
|
|
260
273
|
- **Footer** — field visibility. In fullscreen mode the session name (or `sessions` for an unnamed session) opens the session picker and each subagent/shell row opens the activity view. SoL-Pi's savings status is not repeated in the footer (it already notifies).
|
|
261
|
-
- **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, advisor, advisor gates.
|
|
274
|
+
- **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, **Max subagents** (explicit choice then Save; default 4), advisor, advisor gates.
|
|
262
275
|
|
|
263
276
|
pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
|
|
264
277
|
|
|
@@ -287,7 +300,10 @@ pi-jar/
|
|
|
287
300
|
│ ├── split-view.ts shared two-pane frame
|
|
288
301
|
│ ├── changes.ts change tracker and line diff; diff-view.ts is /diff
|
|
289
302
|
│ ├── shells.ts background shells and jar_shell
|
|
290
|
-
│ ├── delegate.ts
|
|
303
|
+
│ ├── delegate.ts retained subagents, moderator controls and RPC lifecycle
|
|
304
|
+
│ ├── delegate-worktree.ts isolated Git worktrees, path guard and safe reconciliation
|
|
305
|
+
│ ├── discussion.ts bounded structured cross-agent Q/A paper
|
|
306
|
+
│ ├── democracy.ts exceptional scout ballots, strict majority and user tie-break
|
|
291
307
|
│ ├── activity-view.ts subagent/shell details view
|
|
292
308
|
│ ├── icons.ts unicode / nerd / ascii glyph sets
|
|
293
309
|
│ ├── attachments.ts image chips; prompt-search.ts is prompt history
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -78,8 +78,9 @@ Resolution for a role name:
|
|
|
78
78
|
|
|
79
79
|
1. Merge global (`<agent dir>/pi-jar-roles.json`) and project (`<cwd>/.pi/pi-jar-roles.json`) roles; project wins per role.
|
|
80
80
|
2. If the spec is `@other[:effort]`, remember the effort (first one wins) and follow `other`; stop after five hops or on a cycle.
|
|
81
|
-
3. A `provider/model[:effort]` spec resolves through Pi's model registry
|
|
82
|
-
4.
|
|
81
|
+
3. A `provider/model[:effort]` spec resolves through Pi's model registry.
|
|
82
|
+
4. For an assigned role, activation tries the primary then that role's configured fallback specs in order. Delegated agents also choose the first currently available candidate. An explicit empty project fallback list disables the inherited global list.
|
|
83
|
+
5. An unassigned role "follows the current model" (activating it only labels the footer).
|
|
83
84
|
|
|
84
85
|
`activateTemporary(role)` returns a restore function that puts back the previous model, effort and active role — used by plan mode (`plan`), approved-plan execution (`implement`), goal rounds (`implement` / `advisor`).
|
|
85
86
|
|
|
@@ -108,13 +109,21 @@ Manual choices win: a model or effort you select yourself (model picker, cycling
|
|
|
108
109
|
- If a completed turn did not suggest, a single hidden reminder asks for one (never while plan or goal automation owns the next step, and never after an interrupt).
|
|
109
110
|
- The suggestion is sanitized to one line (≤ 160 characters) and kept in memory only. It is cleared by your next prompt, a new run, typing, or branch navigation.
|
|
110
111
|
|
|
112
|
+
## Task tracking discipline
|
|
113
|
+
|
|
114
|
+
- `jar_todo write` is the normal way to establish the checklist for a new multi-step request. It represents the complete current plan, so completed work from an earlier request is naturally dropped.
|
|
115
|
+
- `update` changes one stable task id while preserving omitted fields and subtasks. `remove` deletes one task tree.
|
|
116
|
+
- `append` is for genuinely new work introduced while an existing checklist is still active, especially user steering or an added requirement. When the current checklist is already fully completed, start a fresh list with `write` instead of appending.
|
|
117
|
+
- `start` and `done` deliberately return/render only the single task they changed. Their structured result still carries the bounded checklist (the full list inside subagents) so parent/subagent progress mirroring remains accurate.
|
|
118
|
+
- Exactly one leaf task may be `in_progress`; starting another leaf parks the previous one. Parent status is derived from its subtasks.
|
|
119
|
+
|
|
111
120
|
## Change review
|
|
112
121
|
|
|
113
122
|
- A `tool_call` hook records a file's content right before the agent's **first** `edit` or `write` to it (project files only, up to 200 files, 1 MB each, text only). Later edits keep that baseline, so the review always shows everything since the agent started on the file.
|
|
114
123
|
- `/diff` (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) compares the baseline with the file on disk using a line diff with 3 lines of context. Files that are back to their baseline drop out.
|
|
115
124
|
- **Accept** forgets the baseline. **Revert** writes the baseline back (or removes a file the agent created) after a second key press to confirm. Errors are reported and the file stays listed.
|
|
116
125
|
- Baselines live in memory for the session; changes made through `bash` or by you are not tracked.
|
|
117
|
-
- **Subagent edits** join the same review
|
|
126
|
+
- **Subagent edits** join the same review in two ways. Retained `worktree` writers remain isolated while working, idle or paused. Only `jar_subagent stop` finalizes them: pi-jar verifies that each parent target still equals the private baseline snapshot, copies reviewable safe results back, then captures/marks those parent files in the normal `ChangeTracker`. The deprecated one-shot `write: true` shared-workspace mode keeps its older `PI_JAR_BASELINE_DIR` bridge for compatibility.
|
|
118
127
|
|
|
119
128
|
## Background shells
|
|
120
129
|
|
|
@@ -130,13 +139,26 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
|
|
|
130
139
|
|
|
131
140
|
## Subagents
|
|
132
141
|
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
142
|
+
- The main agent is the **moderator** while a retained fleet exists. A hidden bounded context lists the fleet and tells the parent to decompose, route, peek, steer, resolve disagreements and synthesize instead of duplicating a worker's task. The optional `moderator` role gives this purpose a configurable model without forcing a model switch.
|
|
143
|
+
- `jar_delegate` shares a session-wide **Max subagents** setting in `/jar settings`: exactly 2, 4, 6, 8 or 16, default 4. All retained live children (including idle/paused and live legacy one-shots) count; the moderator does not. Concurrent/reentrant batches reserve capacity atomically and reject over-cap requests before any launch. Lowering the limit preserves the fleet and prevents further spawning until room is available. `scout` runs `pi --mode rpc --no-session`, is read-only and defaults to the `scout` role. `fork` uses `--fork <parent-session>`, is read-only and defaults to `reviewer`. `worktree` uses the same real context fork, defaults to `worker`, and runs in a detached disposable Git worktree. Explicit `task.role` overrides those defaults; each role uses its primary/fallback candidates before falling back to the parent's current model.
|
|
144
|
+
- Retained children do not exit on `agent_settled`. They become `idle`, keeping the same RPC process and model context. `jar_subagent pause` sends `clear_queue` + `abort` and leaves the agent `paused`; `resume` sends a new prompt to that same child. `steer` redirects a working child. `ask` pauses an active task, crosses a settled boundary, then starts a compact BTW-only prompt, so the old task report cannot be mistaken for the answer. It preserves the prior paused state and assigned scope for later resume.
|
|
145
|
+
- `jar_subagent peek` is the moderator's cheap status surface: task, state, model/role/mode, checklist progress, current activity, changed files, remaining checklist leaves, workspace and last report. `stop` is destructive/final: abort an active operation if necessary, close the child, return a handoff (workspace, changed/applied files, remaining work, last report), and finalize safe worktree changes.
|
|
146
|
+
- Worktree setup snapshots the exact starting workspace: `HEAD` plus tracked dirty changes and non-ignored untracked files are committed only inside the detached worktree. The worktree persists across idle/pause/resume. On stop, the parent target must still byte/mode-match the snapshot and the change must be trackable by `/diff`; otherwise nothing is overwritten and the workspace/error is reported. Conflicted workspaces survive report-history trimming; after resolving parent drift, retry `stop`. Finalization waits for RPC stdio closure. Snapshot blobs preserve filesystem bytes without executing clean/smudge filters or hooks. The path guard handles Pi path normalization, rejects lexical/symlink/dangling-link escapes and Git control metadata, checks read-tool filename fallbacks (NFD, curly apostrophes, AM/PM spaces), and rechecks both source and destination ancestors during reconciliation. All replacements/rollback backups are prepared before parent mutation; failed multi-file application restores earlier files. Exceptional rollback failures explicitly report actual applied paths for recovery. Worktree children receive only `read,edit,write,grep,find,ls,jar_todo,jar_discuss`; shell tools are absent. This is filesystem-write isolation, not a network/process sandbox.
|
|
147
|
+
- `jar_discuss` gives moderator and children a session-scoped structured paper. `ask` appends a question with stable id/from/to fields, `answer` references the question id, and `list` reads recent entries. Text is capped at 1600 characters per entry, at most 64 entries are retained, and the serialized UTF-8 JSON paper is strictly bounded to 64 KiB with oldest-entry eviction and atomic rewrites. It is intentionally for terse inter-agent Q/A rather than another transcript.
|
|
148
|
+
- Fork/worktree children inherit the parent model conversation but reset pi-jar goal/plan/todo automation. All child modes inherit explicit extension flags, set `PI_JAR_CHILD=1` to prevent recursive delegation, and share the discussion-paper path/name through environment variables.
|
|
149
|
+
- Progress (current tool, streaming text, tools used, turns, cost) stays in the activity view. Retained `idle` and `paused` agents remain visible in the footer/activity fleet; the newest eight ordinary retired reports remain for review, while unresolved private workspaces remain recoverable until explicit finalization/session cleanup. `s` in the activity view still steers a working subagent and `x` stops the selected agent through the same registry.
|
|
150
|
+
- The deprecated `write: true` mode remains a one-shot shared-workspace compatibility path. New delegated implementation should use `mode: "worktree"`.
|
|
151
|
+
|
|
152
|
+
### Exceptional agentic democracy
|
|
153
|
+
|
|
154
|
+
`jar_democracy` is **not** a default decomposition or decision tool. The moderator must supply `complexity: "super-complex"`, a concrete complexity justification, the persistent issue, at least two distinct failed approaches with evidence, and 2–8 uniquely identified options with tradeoffs. These gates are runtime-validated in addition to prompt guidance; the moderator is responsible for truthful evidence.
|
|
155
|
+
|
|
156
|
+
- Nominate relevant idle/paused **scouts** with `agents`, and set the total electorate with `voters` (default 4, or 2 at a cap of 2; always bounded by the configured cap). Forks, writable workers, busy agents and duplicate nominations are rejected. Reuse preserves context; prefer fresh scouts if discussion history could bias a ballot.
|
|
157
|
+
- Unfilled seats spawn fresh read-only scouts through the normal delegate controller. The same session-wide capacity applies; the moderator must explicitly stop unneeded agents if there is no room. Voting never silently kills existing workers. Voters are retained for follow-up.
|
|
158
|
+
- Each voter investigates independently and returns one nonce-bound option ID with a short evidence-based rationale. Voters are instructed not to use shared discussion. Results are collected before any tally is exposed; malformed/stale/missing/error ballots are recorded, never inferred.
|
|
159
|
+
- A **strict majority** means more than half of **all invited voters**, with at least two valid ballots. The largest plurality is not a majority. Missing ballots do not reduce this denominator or allow a single response to decide.
|
|
160
|
+
- Ties, plurality without majority, and insufficient ballots return `needs-user`. The leading options are presented to the user with their evidence/tradeoffs in the TUI question picker, including custom/chat/cancel paths. In RPC, the moderator must use `jar_ask` before proceeding. Recommend the most plausible option from actual evidence; do not fabricate numerical probabilities or silently break a tie.
|
|
161
|
+
- A majority is a recommendation, not permission to execute dangerous work. The tool never implements the selected option, and ordinary safety/user-approval requirements remain in force. Only one round may run at a time; abort invalidates incomplete results.
|
|
140
162
|
|
|
141
163
|
## Sessions and prompt history
|
|
142
164
|
|
package/extensions/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { Key, truncateToWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
6
6
|
import { ACCENT_NAMES, loadedAccents, selectAccent } from "../src/accent.ts";
|
|
7
7
|
import { registerAskTool } from "../src/ask-tool.ts";
|
|
8
|
+
import { registerDemocracy } from "../src/democracy.ts";
|
|
9
|
+
import type { DelegateController } from "../src/delegate.ts";
|
|
8
10
|
import { ComposerStyle } from "../src/composer.ts";
|
|
9
11
|
import { installCompactBuiltinTools } from "../src/compact-tools.ts";
|
|
10
12
|
import { WELCOME_INTERVAL_MS } from "../src/animations.ts";
|
|
@@ -34,7 +36,9 @@ import { TASK_ENTRY, TodoStore, todoProgress, todoTotals } from "../src/tasks.ts
|
|
|
34
36
|
import { formatCost, sessionCost } from "../src/usage.ts";
|
|
35
37
|
import { WorkingState } from "../src/working.ts";
|
|
36
38
|
import { ChangeTracker } from "../src/changes.ts";
|
|
37
|
-
import { DelegateRegistry, registerDelegate, type DelegateRun } from "../src/delegate.ts";
|
|
39
|
+
import { CHILD_ENV, DelegateRegistry, registerDelegate, type DelegateRun } from "../src/delegate.ts";
|
|
40
|
+
import { CHILD_WORKTREE_ENV, workspacePathAllowed } from "../src/delegate-worktree.ts";
|
|
41
|
+
import { createDiscussionPaper, DISCUSSION_FILE_ENV, disposeDiscussionPaper, registerDiscussionTool, SUBAGENT_NAME_ENV } from "../src/discussion.ts";
|
|
38
42
|
import { openActivityView, type ActivityTarget } from "../src/activity-view.ts";
|
|
39
43
|
import { ICON_SETS, setIconSet, withIcon } from "../src/icons.ts";
|
|
40
44
|
import { collectPrompts, openPromptSearch } from "../src/prompt-search.ts";
|
|
@@ -55,6 +59,40 @@ const LARGE_SESSION_ENTRIES = 800;
|
|
|
55
59
|
/** Entries Pi writes when a fresh session starts; a branch holding only these has no transcript yet. */
|
|
56
60
|
const SESSION_SETUP_ENTRIES = new Set(["thinking_level_change", "model_change", "session_info"]);
|
|
57
61
|
export default function piJar(pi: ExtensionAPI): void {
|
|
62
|
+
const delegatedChild = process.env[CHILD_ENV] === "1";
|
|
63
|
+
const delegatedWorktree = delegatedChild ? process.env[CHILD_WORKTREE_ENV] : undefined;
|
|
64
|
+
if (delegatedChild) {
|
|
65
|
+
// Forked transcripts retain custom prompt messages even though their automation stores
|
|
66
|
+
// and handlers are disabled. Remove only parent automation, not task/user context.
|
|
67
|
+
const parentAutomation = new Set([
|
|
68
|
+
"pi-jar.goal-context", "pi-jar.goal-continuation",
|
|
69
|
+
"pi-jar.plan-context", "pi-jar.plan-reminder", "pi-jar.moderator-context"
|
|
70
|
+
]);
|
|
71
|
+
pi.on("context", (event) => {
|
|
72
|
+
const messages = event.messages.filter((message) => !parentAutomation.has((message as { customType?: string }).customType ?? ""));
|
|
73
|
+
if (messages.length !== event.messages.length) return { messages };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (delegatedWorktree) {
|
|
77
|
+
const pathTools = new Set(["read", "edit", "write", "grep", "find", "ls"]);
|
|
78
|
+
const shellTools = new Set(["bash", "powershell", "jar_shell"]);
|
|
79
|
+
pi.on("tool_call", (event, ctx) => {
|
|
80
|
+
if (shellTools.has(event.toolName)) {
|
|
81
|
+
return { block: true, reason: "Sandboxed worktree subagents cannot run shell tools." };
|
|
82
|
+
}
|
|
83
|
+
if (!pathTools.has(event.toolName)) return;
|
|
84
|
+
const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {};
|
|
85
|
+
if (!workspacePathAllowed(delegatedWorktree, ctx.cwd, ".")) {
|
|
86
|
+
return { block: true, reason: "Sandboxed worktree subagent working directory is outside its workspace." };
|
|
87
|
+
}
|
|
88
|
+
for (const key of ["path", "file_path", "cwd"]) {
|
|
89
|
+
const value = input[key];
|
|
90
|
+
if (typeof value === "string" && value && !workspacePathAllowed(delegatedWorktree, ctx.cwd, value, event.toolName === "read")) {
|
|
91
|
+
return { block: true, reason: `Sandboxed worktree subagent cannot access outside its workspace: ${value}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
58
96
|
installCompactBuiltinTools(pi);
|
|
59
97
|
let demo = false;
|
|
60
98
|
let visualSettings = defaultVisualSettings();
|
|
@@ -94,6 +132,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
94
132
|
let workingIndicatorKey = "";
|
|
95
133
|
let openSettings: (ctx: ExtensionContext) => Promise<void> = async () => {};
|
|
96
134
|
let settingsOpen = false;
|
|
135
|
+
let discussionFile: string | undefined = process.env[DISCUSSION_FILE_ENV];
|
|
136
|
+
registerDiscussionTool(pi, () => discussionFile, () => process.env[SUBAGENT_NAME_ENV] ?? "moderator");
|
|
97
137
|
|
|
98
138
|
// A finished list stays visible (all struck through) until the user's next prompt, like Claude.
|
|
99
139
|
let todosAcknowledged = false;
|
|
@@ -163,14 +203,15 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
163
203
|
/** Finished work stays visible briefly so a quick run is not a flicker. */
|
|
164
204
|
const ACTIVITY_LINGER_MS = 5000;
|
|
165
205
|
const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
|
|
166
|
-
: run.state === "
|
|
206
|
+
: run.state === "idle" ? "idle" : run.state === "paused" ? "paused" : run.state === "stopped" ? "stopped"
|
|
207
|
+
: run.state === "done" ? "done" : "failed";
|
|
167
208
|
const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
|
|
168
209
|
: job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
|
|
169
210
|
const seconds = (ms: number) => { const s = Math.max(0, Math.round(ms / 1000)); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; };
|
|
170
211
|
const footerActivity = (now: number): FooterActivity[] => {
|
|
171
212
|
const rows: FooterActivity[] = [];
|
|
172
213
|
for (const { key, run } of subagents.records()) {
|
|
173
|
-
if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
214
|
+
if ((run.state === "done" || run.state === "failed" || run.state === "stopped") && run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
174
215
|
rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
|
|
175
216
|
detail: run.error ?? run.activity ?? cleanText(run.task, 120),
|
|
176
217
|
stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
|
|
@@ -227,17 +268,21 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
227
268
|
registerAdvisor(pi, modelRoles, { enabled: () => visualSettings.advisor, gates: () => visualSettings.advisorGates, usage: sideUsage });
|
|
228
269
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
229
270
|
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
|
|
230
|
-
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
|
|
231
|
-
|
|
271
|
+
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles), { activateDefault: () => !delegatedChild });
|
|
272
|
+
let delegateController: DelegateController | undefined;
|
|
273
|
+
registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges, discussionFile: () => discussionFile,
|
|
274
|
+
getMaxSubagents: () => visualSettings.maxSubagents, onController: (controller) => { delegateController = controller; } });
|
|
275
|
+
if (!delegatedChild) registerDemocracy(pi, () => delegateController, () => visualSettings.maxSubagents);
|
|
232
276
|
const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
|
|
233
|
-
planMode.register();
|
|
277
|
+
if (!delegatedChild) planMode.register();
|
|
234
278
|
const goalLoop = new GoalLoop(pi, {
|
|
235
279
|
goals: () => goals, todos: () => todos, roles: modelRoles, planActive: () => planMode.isEnabled(),
|
|
236
280
|
maxRounds: () => visualSettings.goalRounds,
|
|
281
|
+
subagentMode: (agent) => subagents.resolve(agent)?.run.mode,
|
|
237
282
|
changed: (ctx) => { footerTui?.requestRender(); welcomeTui?.requestRender(); updateTaskWidget(ctx); },
|
|
238
283
|
completed: () => composer.flash("complete", 4000)
|
|
239
284
|
});
|
|
240
|
-
goalLoop.register();
|
|
285
|
+
if (!delegatedChild) goalLoop.register();
|
|
241
286
|
planMode.setOnEnter((ctx) => goalLoop.pause(ctx, "plan mode is on"));
|
|
242
287
|
const suggestions = new SuggestionState();
|
|
243
288
|
composer.attachSuggestions(suggestions);
|
|
@@ -603,13 +648,22 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
603
648
|
changeCount = 0;
|
|
604
649
|
shells?.dispose();
|
|
605
650
|
subagents.clear();
|
|
651
|
+
if (!delegatedChild) {
|
|
652
|
+
disposeDiscussionPaper(discussionFile);
|
|
653
|
+
discussionFile = createDiscussionPaper();
|
|
654
|
+
}
|
|
606
655
|
shells = new ShellManager(onShellEvent);
|
|
607
656
|
shells.onChange = () => footerTui?.requestRender();
|
|
608
657
|
goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
|
|
609
658
|
let branch: readonly unknown[] = [];
|
|
610
659
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
611
|
-
|
|
612
|
-
|
|
660
|
+
if (delegatedChild) {
|
|
661
|
+
todos.restore([]);
|
|
662
|
+
goals.restore([]);
|
|
663
|
+
} else {
|
|
664
|
+
restoreTodos(ctx, branch);
|
|
665
|
+
restoreGoal(ctx, branch);
|
|
666
|
+
}
|
|
613
667
|
// Quota is enabled per session; no credentials or consent are persisted.
|
|
614
668
|
quotaCache = new QuotaCache(
|
|
615
669
|
(provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
|
|
@@ -641,7 +695,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
641
695
|
// A fresh session is not empty: Pi records the startup thinking level/model as entries.
|
|
642
696
|
const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
|
|
643
697
|
&& typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
|
|
644
|
-
if (!resumed) showWelcome(ctx);
|
|
698
|
+
if (!delegatedChild && !resumed) showWelcome(ctx);
|
|
645
699
|
});
|
|
646
700
|
// Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
|
|
647
701
|
const dismissWelcome = (ctx: ExtensionContext) => {
|
|
@@ -684,13 +738,19 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
684
738
|
pi.on("session_tree", (_event, ctx) => {
|
|
685
739
|
let branch: readonly unknown[] = [];
|
|
686
740
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
687
|
-
|
|
688
|
-
|
|
741
|
+
if (!delegatedChild) {
|
|
742
|
+
restoreTodos(ctx, branch);
|
|
743
|
+
restoreGoal(ctx, branch);
|
|
744
|
+
}
|
|
689
745
|
updateCost(ctx);
|
|
690
746
|
composer.refreshSession(ctx);
|
|
691
747
|
sampleFooter(ctx);
|
|
692
748
|
});
|
|
693
|
-
pi.on("session_compact", (_event, ctx) => {
|
|
749
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
750
|
+
if (!delegatedChild) { restoreTodos(ctx); restoreGoal(ctx); }
|
|
751
|
+
updateCost(ctx);
|
|
752
|
+
sampleFooter(ctx);
|
|
753
|
+
});
|
|
694
754
|
pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
|
|
695
755
|
pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
|
|
696
756
|
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
|
|
@@ -703,6 +763,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
703
763
|
try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
|
|
704
764
|
quotaCache?.stop();
|
|
705
765
|
quotaCache = undefined;
|
|
766
|
+
if (!delegatedChild) { disposeDiscussionPaper(discussionFile); discussionFile = undefined; }
|
|
706
767
|
todos = undefined;
|
|
707
768
|
changes = undefined;
|
|
708
769
|
changeCount = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A warm, role-aware Pi terminal workspace with animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/activity-view.ts
CHANGED
|
@@ -46,7 +46,8 @@ function collect(sources: ActivitySources): Item[] {
|
|
|
46
46
|
if (records.length) items.push({ kind: "header", label: withIcon("subagents", "SUBAGENTS") });
|
|
47
47
|
for (const record of records) {
|
|
48
48
|
const { run } = record;
|
|
49
|
-
const status: Status = run.state === "working" ? "running" : run.state === "queued"
|
|
49
|
+
const status: Status = run.state === "working" ? "running" : run.state === "queued" || run.state === "idle" ? "pending"
|
|
50
|
+
: run.state === "paused" || run.state === "stopped" ? "stopped" : run.state === "done" ? "success" : "error";
|
|
50
51
|
items.push({ kind: "subagent", id: record.key, status, label: run.name, record });
|
|
51
52
|
}
|
|
52
53
|
const jobs = sources.shells?.summaries() ?? [];
|
|
@@ -215,7 +216,7 @@ function subagentHead(run: DelegateRun, status: Status, width: number, fg: Fg):
|
|
|
215
216
|
].filter(Boolean);
|
|
216
217
|
const head = [
|
|
217
218
|
fg("accent", [run.name, run.role, run.model].filter(Boolean).join(" · ")),
|
|
218
|
-
fg(COLOR[status], withIcon(status, status === "stopped" ? "stopped" : run.state)) + fg("dim", " · " + facts.join(" · "))
|
|
219
|
+
fg(COLOR[status], withIcon(status, run.state === "paused" ? "paused" : status === "stopped" ? "stopped" : run.state)) + fg("dim", " · " + facts.join(" · "))
|
|
219
220
|
];
|
|
220
221
|
// A turning glyph on the 1 s tick: a long tool call or a slow model still looks alive.
|
|
221
222
|
if (run.activity) head.push(fg("muted", (run.state === "working" ? SPINNER[Math.floor(Date.now() / 1000) % SPINNER.length] + " " : "") + run.activity));
|
|
@@ -348,9 +349,14 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
|
|
|
348
349
|
};
|
|
349
350
|
const stopSelected = () => {
|
|
350
351
|
const item = selected();
|
|
351
|
-
if (!item || item.kind === "role"
|
|
352
|
+
if (!item || item.kind === "role") return;
|
|
353
|
+
const subagentLive = item.kind === "subagent" && !["done", "failed", "stopped"].includes(item.record.run.state);
|
|
354
|
+
const shellLive = item.kind === "shell" && item.status === "running";
|
|
355
|
+
if (!subagentLive && !shellLive) return;
|
|
352
356
|
try {
|
|
353
|
-
if (item.kind === "subagent") sources.subagents.stop(item.id)
|
|
357
|
+
if (item.kind === "subagent") void sources.subagents.stop(item.id).catch((error: unknown) => {
|
|
358
|
+
ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error");
|
|
359
|
+
});
|
|
354
360
|
else shells?.kill(item.id);
|
|
355
361
|
} catch (error) { ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error"); }
|
|
356
362
|
};
|
|
@@ -445,7 +451,8 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
|
|
|
445
451
|
// Footer: the two actions, the steering input for subagents, then key hints.
|
|
446
452
|
const actions = optionList(theme, [item?.kind === "shell" ? "Kill the selected shell" : item?.kind === "role" ? "Stop (teammates from other extensions are read-only)" : "Stop the selected subagent",
|
|
447
453
|
"Follow the latest output"], -1, ACTIONS.map((action) => action.key));
|
|
448
|
-
const stoppable = item?.kind === "subagent" ?
|
|
454
|
+
const stoppable = item?.kind === "subagent" ? !["done", "failed", "stopped"].includes(item.record.run.state)
|
|
455
|
+
: item?.kind === "shell" && item.status === "running";
|
|
449
456
|
if (!stoppable) actions[0] = fg("dim", stripTerminalSequences(actions[0]!));
|
|
450
457
|
const footer = [...actions];
|
|
451
458
|
if (subagent) {
|
package/src/ask-tool.ts
CHANGED
|
@@ -26,13 +26,18 @@ type PickResult = { kind: "answer"; selected: string[] } | { kind: "custom" } |
|
|
|
26
26
|
|
|
27
27
|
const fit = (line: string, width: number) => truncateToWidth(line, Math.max(0, width));
|
|
28
28
|
|
|
29
|
-
async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index: number, total: number): Promise<PickResult> {
|
|
29
|
+
async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index: number, total: number, signal?: AbortSignal): Promise<PickResult> {
|
|
30
30
|
const options = (question.options ?? []).map((option) => ({
|
|
31
31
|
label: cleanText(option.label, 120),
|
|
32
32
|
description: option.description ? cleanText(option.description, 240) : undefined
|
|
33
33
|
})).filter((option) => !!option.label);
|
|
34
|
-
if (!ctx.hasUI || ctx.mode !== "tui") return { kind: "cancel" };
|
|
35
|
-
|
|
34
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || signal?.aborted) return { kind: "cancel" };
|
|
35
|
+
let abort: (() => void) | undefined;
|
|
36
|
+
try { return await ctx.ui.custom<PickResult>((tui, theme, _keys, complete) => {
|
|
37
|
+
const done = (result: PickResult) => { if (abort) signal?.removeEventListener("abort", abort); complete(result); };
|
|
38
|
+
abort = () => done({ kind: "cancel" });
|
|
39
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
40
|
+
if (signal?.aborted) queueMicrotask(abort);
|
|
36
41
|
const multi = !!question.multi;
|
|
37
42
|
const allowCustom = question.allowCustom !== false;
|
|
38
43
|
const actions = [
|
|
@@ -131,13 +136,13 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
131
136
|
return lines.map((line) => visibleWidth(line) <= width ? line : fit(line, width));
|
|
132
137
|
}
|
|
133
138
|
};
|
|
134
|
-
});
|
|
139
|
+
}); } finally { if (abort) signal?.removeEventListener("abort", abort); }
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
async function askOne(ctx: ExtensionContext, question: AskQuestion, index
|
|
142
|
+
export async function askOne(ctx: ExtensionContext, question: AskQuestion, index = 0, total = 1, signal?: AbortSignal): Promise<AskAnswer> {
|
|
138
143
|
const id = cleanText(question.id ?? "q" + String(index + 1), 64) || "q" + String(index + 1);
|
|
139
|
-
const picked = await pickQuestion(ctx, question, index, total);
|
|
140
|
-
if (!picked || picked.kind === "cancel") return { id, cancelled: true };
|
|
144
|
+
const picked = await pickQuestion(ctx, question, index, total, signal);
|
|
145
|
+
if (signal?.aborted || !picked || picked.kind === "cancel") return { id, cancelled: true };
|
|
141
146
|
if (picked.kind === "custom") {
|
|
142
147
|
const text = await ctx.ui.editor("✎ Your answer · " + cleanText(question.header ?? "Question", 48), "");
|
|
143
148
|
return text?.trim() ? { id, answer: text.trim() } : { id, cancelled: true };
|