pi-jar 0.1.4 → 0.1.6
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 +35 -19
- package/docs/INTEGRATIONS.md +2 -2
- package/docs/WORKFLOWS.md +34 -10
- package/extensions/index.ts +87 -15
- package/package.json +1 -1
- package/src/activity-view.ts +23 -7
- package/src/ask-tool.ts +12 -7
- package/src/compact-tools.ts +66 -28
- package/src/delegate-worktree.ts +360 -0
- package/src/delegate.ts +934 -130
- package/src/democracy.ts +214 -0
- package/src/discussion.ts +189 -0
- package/src/footer.ts +4 -3
- package/src/goal-loop.ts +11 -16
- package/src/model-roles.ts +58 -29
- package/src/plan.ts +4 -10
- 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,20 +246,24 @@ 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 only task progress (done/total, the in-progress task, activity, the next open tasks), read from memory; `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. When a resumed turn ends, a visible `pi-jar.subagent` message wakes the moderator (or queues for its current run) with that progress and the turn's report, so it never polls `peek`. Every action returns on <kbd>Esc</kbd>, `pause` gives up waiting after 15 s, and a working child with no RPC activity for 10 minutes is stopped as hung.
|
|
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. A round is bounded: a 10-minute ballot deadline records unfinished voters as failures, rationales are capped at 600 characters, and scouts spawned for the round are retired when it ends.
|
|
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). `list` replies are capped at 4000 characters; `since` rereads only newer entries and `questionId` reads one thread in full. 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
|
-
- 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.
|
|
266
|
+
- Pi's built-in read/shell/edit/write tool cards render compactly: collapsed, each is one padded, state-colored card with its call line and a one-line summary (edit included), and partially streamed arguments never flicker to a fallback; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away. Background sources (subagent streams, shell output) repaint the footer at most every 250 ms and the activity view every 100 ms, because every requested frame re-renders the whole transcript. The sliding bar some terminals show in the tab while the agent works is Pi's own `OSC 9;4` progress (`terminal.showTerminalProgress` in Pi settings), not pi-jar.
|
|
254
267
|
|
|
255
268
|
## Settings
|
|
256
269
|
|
|
@@ -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/INTEGRATIONS.md
CHANGED
|
@@ -50,7 +50,7 @@ ctx.ui.setStatus("pi-jar.quota.openai-codex", JSON.stringify({
|
|
|
50
50
|
| `pi-jar.goal` | goal events (`set`, `status`, `round`; v1 `set`/`clear` accepted) | active branch |
|
|
51
51
|
| `pi-jar.plan` | plan state (`enabled`, `steps`, `text`, `path`, `title`) | active branch |
|
|
52
52
|
|
|
53
|
-
Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.goal-context`, `pi-jar.goal-continuation`, `pi-jar.suggest-reminder`) are filtered or deduplicated in the `context` hook, so they never accumulate.
|
|
53
|
+
Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.goal-context`, `pi-jar.goal-continuation`, `pi-jar.moderator-context`, `pi-jar.suggest-reminder`) are filtered or deduplicated in the `context` hook, so they never accumulate. Pi applies a `context` rewrite to one LLM call only and re-sends the full persisted history before the next, so the pruning runs on every call.
|
|
54
54
|
|
|
55
55
|
## Tools pi-jar registers
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.
|
|
|
65
65
|
| `jar_delegate` | up to four parallel subagents (`tasks: [{task, name?, role?}]`, `write?`) |
|
|
66
66
|
| `jar_advisor` | second opinion from the `advisor` role (`question?`, `draft?`) |
|
|
67
67
|
|
|
68
|
-
`jar_shell` events arrive as a visible `pi-jar.shell` custom message that triggers (or queues) an agent turn. `jar_delegate` runs child processes with `PI_JAR_CHILD=1`, which keeps pi-jar in the child from registering `jar_delegate` again, and publishes each running subagent through the role contract above as `pi-jar.role.delegate-<batch>-<n>`. The advisor publishes `pi-jar.role.advisor` while it is consulting, and its `/advisor` and gate answers arrive as visible `pi-jar.advisor` custom messages.
|
|
68
|
+
`jar_shell` events arrive as a visible `pi-jar.shell` custom message that triggers (or queues) an agent turn; when a subagent turn started by `jar_subagent resume` ends, a visible `pi-jar.subagent` message does the same. `jar_delegate` runs child processes with `PI_JAR_CHILD=1`, which keeps pi-jar in the child from registering `jar_delegate` again, and publishes each running subagent through the role contract above as `pi-jar.role.delegate-<batch>-<n>`. The advisor publishes `pi-jar.role.advisor` while it is consulting, and its `/advisor` and gate answers arrive as visible `pi-jar.advisor` custom messages.
|
|
69
69
|
|
|
70
70
|
## Commands that overlap other packages
|
|
71
71
|
|
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,28 @@ 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 and reads only task progress from memory: key, name, state, done/total checklist leaves, the in-progress task, current activity and the next three open tasks (no git, transcript or report reads). `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
|
+
- The moderator never has to poll: when a turn started by `jar_subagent resume` ends, pi-jar coalesces the settled agents into one visible `pi-jar.subagent` message (their `peek` progress plus the turn's report, clipped to 1200 characters) that triggers or queues a moderator turn. `pause`, `ask` and `stop` cancel that wake-up because the moderator is handling the agent directly.
|
|
147
|
+
- Nothing in the moderator path waits unbounded. `jar_subagent` returns as soon as the tool call is aborted (the child keeps its state); `pause` reports "not settled yet" after 15 s if the child ignores abort and still pauses when it settles; a working child that sends no RPC event for 10 minutes is killed as hung. After a child exits, its stdio gets 2 s to drain: a grandchild (MCP server, shell) that inherited the pipes cannot keep `stop` or session shutdown waiting for `close`.
|
|
148
|
+
- 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.
|
|
149
|
+
- `jar_discuss` gives moderator and children a session-scoped structured paper. `ask` appends a question with stable id/from/to fields and `answer` references the question id (validated under the same lock as the write). `list` returns the newest entries within a 4000-character reply, each clipped to 400 characters, and says how many older entries it omitted; `since: "d12"` returns only newer entries (a no-change reread costs one line) and `questionId` reads one thread in full. Text is capped at 1600 characters per entry, at most 64 entries are retained, and the compact UTF-8 JSON paper is strictly bounded to 64 KiB with oldest-entry eviction and atomic rewrites. The file lock waits by yielding to the event loop (at most ~1 s), never blocking the UI. It is intentionally for terse inter-agent Q/A rather than another transcript; agents are told not to poll it.
|
|
150
|
+
- 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.
|
|
151
|
+
- 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.
|
|
152
|
+
- The deprecated `write: true` mode remains a one-shot shared-workspace compatibility path. New delegated implementation should use `mode: "worktree"`.
|
|
153
|
+
|
|
154
|
+
### Exceptional agentic democracy
|
|
155
|
+
|
|
156
|
+
`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.
|
|
157
|
+
|
|
158
|
+
- 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.
|
|
159
|
+
- 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. Scouts spawned for the round are retired once it ends, so they never keep holding pool slots; nominated scouts stay retained.
|
|
160
|
+
- Each voter checks the evidence briefly and independently and returns one nonce-bound option ID with a rationale (at most 600 characters; failure reasons 200). 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. A round has a 10-minute ballot deadline: unfinished voters are cancelled and recorded as failures (a nominated scout cancelled this way is retired), so a hung voter can never hold the round open.
|
|
161
|
+
- 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.
|
|
162
|
+
- 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.
|
|
163
|
+
- 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
164
|
|
|
141
165
|
## Sessions and prompt history
|
|
142
166
|
|
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, SUBAGENT_MESSAGE, 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", SUBAGENT_MESSAGE
|
|
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;
|
|
@@ -158,19 +198,31 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
158
198
|
};
|
|
159
199
|
/** Footer indicators owned by pi-jar; live subagents and shells get their own rows. */
|
|
160
200
|
const footerChips = () => changeCount ? [withIcon("changes", `${changeCount} file${changeCount === 1 ? "" : "s"} · /diff`)] : [];
|
|
201
|
+
/**
|
|
202
|
+
* Subagent streams and shell output change the footer many times a second; every requested frame
|
|
203
|
+
* re-renders the whole transcript, so background sources repaint it at most this often.
|
|
204
|
+
*/
|
|
205
|
+
const BACKGROUND_REPAINT_MS = 250;
|
|
206
|
+
let backgroundRepaint: NodeJS.Timeout | undefined;
|
|
207
|
+
const repaintFooterSoon = () => {
|
|
208
|
+
if (backgroundRepaint) return;
|
|
209
|
+
backgroundRepaint = setTimeout(() => { backgroundRepaint = undefined; footerTui?.requestRender(); }, BACKGROUND_REPAINT_MS);
|
|
210
|
+
backgroundRepaint.unref?.();
|
|
211
|
+
};
|
|
161
212
|
const subagents = new DelegateRegistry();
|
|
162
|
-
subagents.subscribe(
|
|
213
|
+
subagents.subscribe(repaintFooterSoon);
|
|
163
214
|
/** Finished work stays visible briefly so a quick run is not a flicker. */
|
|
164
215
|
const ACTIVITY_LINGER_MS = 5000;
|
|
165
216
|
const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
|
|
166
|
-
: run.state === "
|
|
217
|
+
: run.state === "idle" ? "idle" : run.state === "paused" ? "paused" : run.state === "stopped" ? "stopped"
|
|
218
|
+
: run.state === "done" ? "done" : "failed";
|
|
167
219
|
const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
|
|
168
220
|
: job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
|
|
169
221
|
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
222
|
const footerActivity = (now: number): FooterActivity[] => {
|
|
171
223
|
const rows: FooterActivity[] = [];
|
|
172
224
|
for (const { key, run } of subagents.records()) {
|
|
173
|
-
if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
225
|
+
if ((run.state === "done" || run.state === "failed" || run.state === "stopped") && run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
174
226
|
rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
|
|
175
227
|
detail: run.error ?? run.activity ?? cleanText(run.task, 120),
|
|
176
228
|
stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
|
|
@@ -227,17 +279,21 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
227
279
|
registerAdvisor(pi, modelRoles, { enabled: () => visualSettings.advisor, gates: () => visualSettings.advisorGates, usage: sideUsage });
|
|
228
280
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
229
281
|
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
|
|
230
|
-
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
|
|
231
|
-
|
|
282
|
+
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles), { activateDefault: () => !delegatedChild });
|
|
283
|
+
let delegateController: DelegateController | undefined;
|
|
284
|
+
registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges, discussionFile: () => discussionFile,
|
|
285
|
+
getMaxSubagents: () => visualSettings.maxSubagents, onController: (controller) => { delegateController = controller; } });
|
|
286
|
+
if (!delegatedChild) registerDemocracy(pi, () => delegateController, () => visualSettings.maxSubagents);
|
|
232
287
|
const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
|
|
233
|
-
planMode.register();
|
|
288
|
+
if (!delegatedChild) planMode.register();
|
|
234
289
|
const goalLoop = new GoalLoop(pi, {
|
|
235
290
|
goals: () => goals, todos: () => todos, roles: modelRoles, planActive: () => planMode.isEnabled(),
|
|
236
291
|
maxRounds: () => visualSettings.goalRounds,
|
|
292
|
+
subagentMode: (agent) => subagents.resolve(agent)?.run.mode,
|
|
237
293
|
changed: (ctx) => { footerTui?.requestRender(); welcomeTui?.requestRender(); updateTaskWidget(ctx); },
|
|
238
294
|
completed: () => composer.flash("complete", 4000)
|
|
239
295
|
});
|
|
240
|
-
goalLoop.register();
|
|
296
|
+
if (!delegatedChild) goalLoop.register();
|
|
241
297
|
planMode.setOnEnter((ctx) => goalLoop.pause(ctx, "plan mode is on"));
|
|
242
298
|
const suggestions = new SuggestionState();
|
|
243
299
|
composer.attachSuggestions(suggestions);
|
|
@@ -603,13 +659,22 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
603
659
|
changeCount = 0;
|
|
604
660
|
shells?.dispose();
|
|
605
661
|
subagents.clear();
|
|
662
|
+
if (!delegatedChild) {
|
|
663
|
+
disposeDiscussionPaper(discussionFile);
|
|
664
|
+
discussionFile = createDiscussionPaper();
|
|
665
|
+
}
|
|
606
666
|
shells = new ShellManager(onShellEvent);
|
|
607
|
-
shells.onChange =
|
|
667
|
+
shells.onChange = repaintFooterSoon;
|
|
608
668
|
goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
|
|
609
669
|
let branch: readonly unknown[] = [];
|
|
610
670
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
611
|
-
|
|
612
|
-
|
|
671
|
+
if (delegatedChild) {
|
|
672
|
+
todos.restore([]);
|
|
673
|
+
goals.restore([]);
|
|
674
|
+
} else {
|
|
675
|
+
restoreTodos(ctx, branch);
|
|
676
|
+
restoreGoal(ctx, branch);
|
|
677
|
+
}
|
|
613
678
|
// Quota is enabled per session; no credentials or consent are persisted.
|
|
614
679
|
quotaCache = new QuotaCache(
|
|
615
680
|
(provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
|
|
@@ -641,7 +706,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
641
706
|
// A fresh session is not empty: Pi records the startup thinking level/model as entries.
|
|
642
707
|
const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
|
|
643
708
|
&& typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
|
|
644
|
-
if (!resumed) showWelcome(ctx);
|
|
709
|
+
if (!delegatedChild && !resumed) showWelcome(ctx);
|
|
645
710
|
});
|
|
646
711
|
// Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
|
|
647
712
|
const dismissWelcome = (ctx: ExtensionContext) => {
|
|
@@ -684,13 +749,19 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
684
749
|
pi.on("session_tree", (_event, ctx) => {
|
|
685
750
|
let branch: readonly unknown[] = [];
|
|
686
751
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
687
|
-
|
|
688
|
-
|
|
752
|
+
if (!delegatedChild) {
|
|
753
|
+
restoreTodos(ctx, branch);
|
|
754
|
+
restoreGoal(ctx, branch);
|
|
755
|
+
}
|
|
689
756
|
updateCost(ctx);
|
|
690
757
|
composer.refreshSession(ctx);
|
|
691
758
|
sampleFooter(ctx);
|
|
692
759
|
});
|
|
693
|
-
pi.on("session_compact", (_event, ctx) => {
|
|
760
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
761
|
+
if (!delegatedChild) { restoreTodos(ctx); restoreGoal(ctx); }
|
|
762
|
+
updateCost(ctx);
|
|
763
|
+
sampleFooter(ctx);
|
|
764
|
+
});
|
|
694
765
|
pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
|
|
695
766
|
pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
|
|
696
767
|
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
|
|
@@ -703,6 +774,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
703
774
|
try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
|
|
704
775
|
quotaCache?.stop();
|
|
705
776
|
quotaCache = undefined;
|
|
777
|
+
if (!delegatedChild) { disposeDiscussionPaper(discussionFile); discussionFile = undefined; }
|
|
706
778
|
todos = undefined;
|
|
707
779
|
changes = undefined;
|
|
708
780
|
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.6",
|
|
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));
|
|
@@ -274,13 +275,22 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
|
|
|
274
275
|
const fg: Fg = (color, text) => theme.fg(color as never, text);
|
|
275
276
|
const shells = sources.shells;
|
|
276
277
|
const previous = shells?.onChange;
|
|
277
|
-
|
|
278
|
-
|
|
278
|
+
// Live streams notify far faster than a transcript view needs to repaint; ~10 frames a second reads as live.
|
|
279
|
+
let repaint: NodeJS.Timeout | undefined;
|
|
280
|
+
const repaintSoon = () => {
|
|
281
|
+
if (repaint || closed) return;
|
|
282
|
+
repaint = setTimeout(() => { repaint = undefined; if (!closed) tui.requestRender(); }, 100);
|
|
283
|
+
repaint.unref?.();
|
|
284
|
+
};
|
|
285
|
+
const unsubscribe = sources.subagents.subscribe(repaintSoon);
|
|
286
|
+
if (shells) shells.onChange = () => { previous?.(); repaintSoon(); };
|
|
279
287
|
const cleanup = () => {
|
|
280
288
|
if (closed) return;
|
|
281
289
|
closed = true;
|
|
282
290
|
clearInterval(timer);
|
|
283
291
|
timer = undefined;
|
|
292
|
+
clearTimeout(repaint);
|
|
293
|
+
repaint = undefined;
|
|
284
294
|
unsubscribe();
|
|
285
295
|
if (shells) shells.onChange = previous;
|
|
286
296
|
};
|
|
@@ -348,9 +358,14 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
|
|
|
348
358
|
};
|
|
349
359
|
const stopSelected = () => {
|
|
350
360
|
const item = selected();
|
|
351
|
-
if (!item || item.kind === "role"
|
|
361
|
+
if (!item || item.kind === "role") return;
|
|
362
|
+
const subagentLive = item.kind === "subagent" && !["done", "failed", "stopped"].includes(item.record.run.state);
|
|
363
|
+
const shellLive = item.kind === "shell" && item.status === "running";
|
|
364
|
+
if (!subagentLive && !shellLive) return;
|
|
352
365
|
try {
|
|
353
|
-
if (item.kind === "subagent") sources.subagents.stop(item.id)
|
|
366
|
+
if (item.kind === "subagent") void sources.subagents.stop(item.id).catch((error: unknown) => {
|
|
367
|
+
ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error");
|
|
368
|
+
});
|
|
354
369
|
else shells?.kill(item.id);
|
|
355
370
|
} catch (error) { ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error"); }
|
|
356
371
|
};
|
|
@@ -445,7 +460,8 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
|
|
|
445
460
|
// Footer: the two actions, the steering input for subagents, then key hints.
|
|
446
461
|
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
462
|
"Follow the latest output"], -1, ACTIONS.map((action) => action.key));
|
|
448
|
-
const stoppable = item?.kind === "subagent" ?
|
|
463
|
+
const stoppable = item?.kind === "subagent" ? !["done", "failed", "stopped"].includes(item.record.run.state)
|
|
464
|
+
: item?.kind === "shell" && item.status === "running";
|
|
449
465
|
if (!stoppable) actions[0] = fg("dim", stripTerminalSequences(actions[0]!));
|
|
450
466
|
const footer = [...actions];
|
|
451
467
|
if (subagent) {
|