pi-jar 0.1.3 → 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 +57 -15
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/WORKFLOWS.md +32 -7
- package/extensions/index.ts +127 -48
- package/package.json +1 -1
- package/src/activity-view.ts +324 -44
- package/src/advisor.ts +9 -3
- package/src/ask-tool.ts +12 -7
- package/src/changes.ts +35 -11
- package/src/child-baselines.ts +42 -0
- package/src/compact-tools.ts +58 -24
- package/src/composer.ts +39 -12
- package/src/delegate-worktree.ts +360 -0
- package/src/delegate.ts +1137 -129
- package/src/democracy.ts +192 -0
- package/src/dialogs.ts +9 -5
- package/src/diff-view.ts +13 -1
- package/src/discussion.ts +153 -0
- package/src/flame.ts +18 -5
- package/src/footer.ts +38 -19
- package/src/goal-loop.ts +8 -1
- package/src/history-ui.ts +19 -12
- package/src/history.ts +3 -1
- package/src/model-roles.ts +120 -36
- package/src/panel.ts +5 -1
- package/src/prompt-search.ts +16 -8
- package/src/roles-ui.ts +18 -10
- package/src/row-cache.ts +26 -0
- package/src/session-ui.ts +9 -4
- package/src/settings-ui.ts +46 -24
- package/src/settings.ts +9 -1
- package/src/shells.ts +2 -1
- package/src/side-model.ts +45 -25
- package/src/split-view.ts +29 -12
- package/src/status.ts +2 -1
- package/src/task-tool.ts +156 -54
- package/src/tasks-ui.ts +15 -5
- package/src/tasks.ts +224 -38
- package/src/welcome.ts +26 -13
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
|
-
| **Tasks & questions** | A Claude-style `jar_todo` checklist the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
|
|
26
|
-
| **Change review** | Every file the agent
|
|
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
|
+
| **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,6 +217,26 @@ 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
|
|
|
220
|
+
### Role fallback models
|
|
221
|
+
|
|
222
|
+
Every role can have an ordered fallback chain. The primary remains configured with `/roles set ROLE`:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
/roles fallback advisor openai/gpt-5:high @smol
|
|
226
|
+
/roles fallback advisor
|
|
227
|
+
/roles fallback advisor clear
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Add `--project` when saving or clearing to override the global chain for this project. Configuration is stored in `pi-jar-roles.json` alongside `roles`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
"fallbacks": { "advisor": ["openai/gpt-5:high", "@smol"] }
|
|
234
|
+
```
|
|
235
|
+
|
|
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.
|
|
237
|
+
|
|
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.
|
|
239
|
+
|
|
211
240
|
## Usage and context
|
|
212
241
|
|
|
213
242
|
`/usage` and `/context` open one tabbed panel (<kbd>Tab</kbd> switches, <kbd>Esc</kbd> closes):
|
|
@@ -217,12 +246,22 @@ The advisor is a second model (the `advisor` role; the current model if unassign
|
|
|
217
246
|
|
|
218
247
|
## Tasks, questions and history
|
|
219
248
|
|
|
220
|
-
- **`jar_todo`** — a Claude-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.
|
|
250
|
+
```json
|
|
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" }] }
|
|
254
|
+
```
|
|
255
|
+
Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
|
|
221
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.
|
|
222
|
-
- **`/diff`** — review what the agent changed since
|
|
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.
|
|
223
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.
|
|
224
|
-
- **`jar_delegate`** — up to
|
|
225
|
-
-
|
|
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.
|
|
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.
|
|
226
265
|
- **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
|
|
227
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.
|
|
228
267
|
|
|
@@ -232,7 +271,7 @@ The advisor is a second model (the `advisor` role; the current model if unassign
|
|
|
232
271
|
|
|
233
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).
|
|
234
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).
|
|
235
|
-
- **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.
|
|
236
275
|
|
|
237
276
|
pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
|
|
238
277
|
|
|
@@ -261,7 +300,10 @@ pi-jar/
|
|
|
261
300
|
│ ├── split-view.ts shared two-pane frame
|
|
262
301
|
│ ├── changes.ts change tracker and line diff; diff-view.ts is /diff
|
|
263
302
|
│ ├── shells.ts background shells and jar_shell
|
|
264
|
-
│ ├── 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
|
|
265
307
|
│ ├── activity-view.ts subagent/shell details view
|
|
266
308
|
│ ├── icons.ts unicode / nerd / ascii glyph sets
|
|
267
309
|
│ ├── attachments.ts image chips; prompt-search.ts is prompt history
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -58,7 +58,7 @@ Then check:
|
|
|
58
58
|
- the composer: typing `/pl` shows autocomplete below the frame; long drafts grow before scrolling; Ember changes mood while dialogs are open;
|
|
59
59
|
- `/jar settings`: Tab through Appearance → Footer → Pi; cycle Unicode/Nerd Font/ASCII icons and toggle mouse mode in the temporary agent dir;
|
|
60
60
|
- `/jar sessions`: search, move between rows, inspect the selected prompt/goal/plan, then resume; the footer session name or `sessions` indicator opens the same picker;
|
|
61
|
-
- `/jar activity`: select a subagent or background shell, scroll its live transcript/output, pause and resume follow, and stop/kill only the selected run;
|
|
61
|
+
- `/jar activity`: select a subagent or background shell, scroll its live transcript/output, pause and resume follow, `Tab` into a subagent's transcript and expand/collapse entries, steer it with `s`, and stop/kill only the selected run;
|
|
62
62
|
- `/roles`: the split manager, `n` new role, `a` alias, `s` scope;
|
|
63
63
|
- `/plan`: the status shows `◆ PLAN · read-only` and a directory appears under `$TMPDIR/pi-jar/plans/`.
|
|
64
64
|
|
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,12 +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.
|
|
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.
|
|
117
127
|
|
|
118
128
|
## Background shells
|
|
119
129
|
|
|
@@ -129,11 +139,26 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
|
|
|
129
139
|
|
|
130
140
|
## Subagents
|
|
131
141
|
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
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.
|
|
137
162
|
|
|
138
163
|
## Sessions and prompt history
|
|
139
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";
|
|
@@ -30,11 +32,13 @@ import { createDemoRoles } from "../src/roles.ts";
|
|
|
30
32
|
import { ACTIVE_STATES, cleanText, collectStatuses, type JarRole } from "../src/status.ts";
|
|
31
33
|
import { manageTasks } from "../src/tasks-ui.ts";
|
|
32
34
|
import { registerTaskTool, todoRow } from "../src/task-tool.ts";
|
|
33
|
-
import { TASK_ENTRY, TodoStore } from "../src/tasks.ts";
|
|
35
|
+
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();
|
|
@@ -62,6 +100,17 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
62
100
|
let enabled = visualSettings.ui;
|
|
63
101
|
let disposeFooter: (() => void) | undefined;
|
|
64
102
|
let footerTui: { requestRender(): void } | undefined;
|
|
103
|
+
/** Footer facts that walk the session to compute; refreshed on the events that change them, never per frame. */
|
|
104
|
+
let footerContext = "ctx ?";
|
|
105
|
+
let footerSessionName: string | undefined;
|
|
106
|
+
const sampleFooter = (ctx: ExtensionContext) => {
|
|
107
|
+
try {
|
|
108
|
+
const usage = ctx.getContextUsage();
|
|
109
|
+
footerContext = usage?.percent == null || !Number.isFinite(usage.percent) ? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
|
|
110
|
+
} catch { footerContext = "ctx ?"; }
|
|
111
|
+
try { footerSessionName = ctx.sessionManager?.getSessionName?.(); } catch { footerSessionName = undefined; }
|
|
112
|
+
footerTui?.requestRender();
|
|
113
|
+
};
|
|
65
114
|
let quotaCache: QuotaCache | undefined;
|
|
66
115
|
let cost = 0;
|
|
67
116
|
let welcomeInterval: ReturnType<typeof setInterval> | undefined;
|
|
@@ -83,6 +132,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
83
132
|
let workingIndicatorKey = "";
|
|
84
133
|
let openSettings: (ctx: ExtensionContext) => Promise<void> = async () => {};
|
|
85
134
|
let settingsOpen = false;
|
|
135
|
+
let discussionFile: string | undefined = process.env[DISCUSSION_FILE_ENV];
|
|
136
|
+
registerDiscussionTool(pi, () => discussionFile, () => process.env[SUBAGENT_NAME_ENV] ?? "moderator");
|
|
86
137
|
|
|
87
138
|
// A finished list stays visible (all struck through) until the user's next prompt, like Claude.
|
|
88
139
|
let todosAcknowledged = false;
|
|
@@ -93,27 +144,37 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
93
144
|
if (open.length) todosAcknowledged = false;
|
|
94
145
|
const visible = enabled && items.length > 0 && (open.length > 0 || !todosAcknowledged);
|
|
95
146
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
// Rows are rebuilt only when the list changes (every change reinstalls the widget), the width or the theme.
|
|
148
|
+
ctx.ui.setWidget("pi-jar.todos", !visible ? undefined : (_tui, theme) => {
|
|
149
|
+
let memo: { width: number; sample: string; lines: string[] } | undefined;
|
|
150
|
+
return {
|
|
151
|
+
invalidate() { memo = undefined; },
|
|
152
|
+
render(width: number) {
|
|
153
|
+
const colors = ctx.ui.theme ?? theme;
|
|
154
|
+
const sample = colors.fg("accent", "·") + colors.fg("dim", "·");
|
|
155
|
+
if (memo && memo.width === width && memo.sample === sample) return memo.lines.slice();
|
|
156
|
+
const all = todos?.all() ?? [];
|
|
157
|
+
const { done, total } = todoTotals(all);
|
|
158
|
+
const parents = new Set<string>();
|
|
159
|
+
for (const item of all) if (item.parentId) parents.add(item.parentId);
|
|
160
|
+
// Show a window of up to 8 rows that keeps the running leaf (or next open one) in view.
|
|
161
|
+
const running = all.findIndex((item) => item.status === "in_progress" && !parents.has(item.id));
|
|
162
|
+
const focus = Math.max(0, running >= 0 ? running : all.findIndex((item) => !item.done && !parents.has(item.id)));
|
|
163
|
+
const start = Math.max(0, Math.min(focus - 2, all.length - 8));
|
|
164
|
+
const shown = all.slice(start, start + 8);
|
|
165
|
+
const rows = [
|
|
166
|
+
colors.fg("accent", "Tasks") + colors.fg("dim", ` · ${done}/${total} done` + (done === total ? " · all complete" : "") + " · /jar tasks"),
|
|
167
|
+
...(start > 0 ? [colors.fg("dim", ` … ${start} earlier`)] : []),
|
|
168
|
+
...shown.map((item) => todoRow(item, (color, text) => colors.fg(color, text), (text) => colors.bold(text), parents.has(item.id) ? todoProgress(all, item.id) : undefined)),
|
|
169
|
+
...(start + shown.length < all.length ? [colors.fg("dim", ` … +${all.length - start - shown.length} more`)] : [])
|
|
170
|
+
];
|
|
171
|
+
// Pi inserts a spacer before widgets, but not between widgets and the composer.
|
|
172
|
+
const lines = [...rows.map((line) => truncateToWidth(line, Math.max(0, width))), truncateToWidth(" ", Math.max(0, width))];
|
|
173
|
+
memo = { width, sample, lines };
|
|
174
|
+
return lines.slice();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
});
|
|
117
178
|
} catch { /* Optional widget; task data remains available via /jar tasks and jar_todo. */ }
|
|
118
179
|
};
|
|
119
180
|
registerTaskTool(pi, () => todos, (ctx) => updateTaskWidget(ctx));
|
|
@@ -142,14 +203,15 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
142
203
|
/** Finished work stays visible briefly so a quick run is not a flicker. */
|
|
143
204
|
const ACTIVITY_LINGER_MS = 5000;
|
|
144
205
|
const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
|
|
145
|
-
: run.state === "
|
|
206
|
+
: run.state === "idle" ? "idle" : run.state === "paused" ? "paused" : run.state === "stopped" ? "stopped"
|
|
207
|
+
: run.state === "done" ? "done" : "failed";
|
|
146
208
|
const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
|
|
147
209
|
: job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
|
|
148
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`; };
|
|
149
211
|
const footerActivity = (now: number): FooterActivity[] => {
|
|
150
212
|
const rows: FooterActivity[] = [];
|
|
151
213
|
for (const { key, run } of subagents.records()) {
|
|
152
|
-
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;
|
|
153
215
|
rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
|
|
154
216
|
detail: run.error ?? run.activity ?? cleanText(run.task, 120),
|
|
155
217
|
stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
|
|
@@ -206,17 +268,21 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
206
268
|
registerAdvisor(pi, modelRoles, { enabled: () => visualSettings.advisor, gates: () => visualSettings.advisorGates, usage: sideUsage });
|
|
207
269
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
208
270
|
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
|
|
209
|
-
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
|
|
210
|
-
|
|
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);
|
|
211
276
|
const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
|
|
212
|
-
planMode.register();
|
|
277
|
+
if (!delegatedChild) planMode.register();
|
|
213
278
|
const goalLoop = new GoalLoop(pi, {
|
|
214
279
|
goals: () => goals, todos: () => todos, roles: modelRoles, planActive: () => planMode.isEnabled(),
|
|
215
280
|
maxRounds: () => visualSettings.goalRounds,
|
|
281
|
+
subagentMode: (agent) => subagents.resolve(agent)?.run.mode,
|
|
216
282
|
changed: (ctx) => { footerTui?.requestRender(); welcomeTui?.requestRender(); updateTaskWidget(ctx); },
|
|
217
283
|
completed: () => composer.flash("complete", 4000)
|
|
218
284
|
});
|
|
219
|
-
goalLoop.register();
|
|
285
|
+
if (!delegatedChild) goalLoop.register();
|
|
220
286
|
planMode.setOnEnter((ctx) => goalLoop.pause(ctx, "plan mode is on"));
|
|
221
287
|
const suggestions = new SuggestionState();
|
|
222
288
|
composer.attachSuggestions(suggestions);
|
|
@@ -420,8 +486,6 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
420
486
|
let expiryAt: number | undefined;
|
|
421
487
|
let memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
422
488
|
let memorySampledAt = 0;
|
|
423
|
-
let context = "ctx ?";
|
|
424
|
-
let contextSampledAt = 0;
|
|
425
489
|
let disposed = false;
|
|
426
490
|
let hits: FooterHit[] = [];
|
|
427
491
|
let pressed: { target: FooterTarget; at: number } | undefined;
|
|
@@ -473,13 +537,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
473
537
|
// Do not schedule cosmetic footer repaints. Role state still updates on Pi's native
|
|
474
538
|
// stream/tool renders, which keeps long transcripts responsive.
|
|
475
539
|
const active = animations && footerSettings.roles && roles.some((role) => ACTIVE_STATES.has(role.state));
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
const usage = ctx.getContextUsage();
|
|
479
|
-
context = usage?.percent == null || !Number.isFinite(usage.percent)
|
|
480
|
-
? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
|
|
481
|
-
contextSampledAt = now;
|
|
482
|
-
}
|
|
540
|
+
// Role glyphs advance with time, not with the render rate, so the footer memo still hits between steps.
|
|
541
|
+
if (active) frame = Math.floor(now / 250);
|
|
483
542
|
if (now - memorySampledAt >= 5000) {
|
|
484
543
|
memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
485
544
|
memorySampledAt = now;
|
|
@@ -487,9 +546,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
487
546
|
const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, now) : undefined;
|
|
488
547
|
const layout = renderFooterLayout({
|
|
489
548
|
model: ctx.model?.id ?? "no-model", effort: ctx.model?.reasoning === false ? "off" : (pi.getThinkingLevel?.() ?? "off"),
|
|
490
|
-
sessionName:
|
|
549
|
+
sessionName: footerSessionName,
|
|
491
550
|
cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
|
|
492
|
-
context, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
|
|
551
|
+
context: footerContext, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
|
|
493
552
|
memory, cost: formatCost(cost), quota, roles, extras: live.extras,
|
|
494
553
|
demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
|
|
495
554
|
}, width, ctx.ui.theme ?? theme);
|
|
@@ -589,13 +648,22 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
589
648
|
changeCount = 0;
|
|
590
649
|
shells?.dispose();
|
|
591
650
|
subagents.clear();
|
|
651
|
+
if (!delegatedChild) {
|
|
652
|
+
disposeDiscussionPaper(discussionFile);
|
|
653
|
+
discussionFile = createDiscussionPaper();
|
|
654
|
+
}
|
|
592
655
|
shells = new ShellManager(onShellEvent);
|
|
593
656
|
shells.onChange = () => footerTui?.requestRender();
|
|
594
657
|
goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
|
|
595
658
|
let branch: readonly unknown[] = [];
|
|
596
659
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
597
|
-
|
|
598
|
-
|
|
660
|
+
if (delegatedChild) {
|
|
661
|
+
todos.restore([]);
|
|
662
|
+
goals.restore([]);
|
|
663
|
+
} else {
|
|
664
|
+
restoreTodos(ctx, branch);
|
|
665
|
+
restoreGoal(ctx, branch);
|
|
666
|
+
}
|
|
599
667
|
// Quota is enabled per session; no credentials or consent are persisted.
|
|
600
668
|
quotaCache = new QuotaCache(
|
|
601
669
|
(provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
|
|
@@ -609,6 +677,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
609
677
|
quotaDisabledByUser = false;
|
|
610
678
|
updateCost(ctx);
|
|
611
679
|
installUi(ctx);
|
|
680
|
+
sampleFooter(ctx);
|
|
612
681
|
composer.setMascot(visualSettings.mascot);
|
|
613
682
|
if (visualSettings.composer && enabled) composer.enable(ctx);
|
|
614
683
|
suggest.sync();
|
|
@@ -626,7 +695,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
626
695
|
// A fresh session is not empty: Pi records the startup thinking level/model as entries.
|
|
627
696
|
const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
|
|
628
697
|
&& typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
|
|
629
|
-
if (!resumed) showWelcome(ctx);
|
|
698
|
+
if (!delegatedChild && !resumed) showWelcome(ctx);
|
|
630
699
|
});
|
|
631
700
|
// Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
|
|
632
701
|
const dismissWelcome = (ctx: ExtensionContext) => {
|
|
@@ -649,6 +718,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
649
718
|
pi.on("agent_start", (_event, ctx) => { dismissWelcome(ctx); working.start(); applyWorking(ctx); });
|
|
650
719
|
pi.on("turn_start", (_event, ctx) => { working.start(); applyWorking(ctx); });
|
|
651
720
|
pi.on("message_end", (event, ctx) => {
|
|
721
|
+
// Every message (user, assistant, tool result) moves the context window.
|
|
722
|
+
sampleFooter(ctx);
|
|
652
723
|
if (event.message.role === "assistant") {
|
|
653
724
|
working.reportOutputTokens(event.message.usage?.output ?? 0);
|
|
654
725
|
addMessageCost(event.message);
|
|
@@ -667,15 +738,22 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
667
738
|
pi.on("session_tree", (_event, ctx) => {
|
|
668
739
|
let branch: readonly unknown[] = [];
|
|
669
740
|
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
670
|
-
|
|
671
|
-
|
|
741
|
+
if (!delegatedChild) {
|
|
742
|
+
restoreTodos(ctx, branch);
|
|
743
|
+
restoreGoal(ctx, branch);
|
|
744
|
+
}
|
|
672
745
|
updateCost(ctx);
|
|
673
746
|
composer.refreshSession(ctx);
|
|
747
|
+
sampleFooter(ctx);
|
|
748
|
+
});
|
|
749
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
750
|
+
if (!delegatedChild) { restoreTodos(ctx); restoreGoal(ctx); }
|
|
751
|
+
updateCost(ctx);
|
|
752
|
+
sampleFooter(ctx);
|
|
674
753
|
});
|
|
675
|
-
pi.on("
|
|
676
|
-
pi.on("model_select", (_event, _ctx) => footerTui?.requestRender());
|
|
754
|
+
pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
|
|
677
755
|
pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
|
|
678
|
-
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx);
|
|
756
|
+
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
|
|
679
757
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
680
758
|
clearSessionBranchCache(ctx);
|
|
681
759
|
stopWelcome(ctx);
|
|
@@ -685,6 +763,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
685
763
|
try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
|
|
686
764
|
quotaCache?.stop();
|
|
687
765
|
quotaCache = undefined;
|
|
766
|
+
if (!delegatedChild) { disposeDiscussionPaper(discussionFile); discussionFile = undefined; }
|
|
688
767
|
todos = undefined;
|
|
689
768
|
changes = undefined;
|
|
690
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",
|