pi-jar 0.1.2 → 0.1.3

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 CHANGED
@@ -24,10 +24,10 @@ The showcase is captured from a real pi-jar terminal session: the torch-style `
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-style `jar_todo` checklist 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 edits is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files next to a colored diff, with accept or revert per file or all at once. |
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. `/jar shells` tails or kills them. |
28
- | **Subagents** | `jar_delegate` fans out up to four read-only (or opt-in editing) subagents in parallel on your model roles; they show up live as teammates on the welcome card and footer. |
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** | `jar_delegate` fans out up to four read-only (or opt-in editing) subagents in parallel on your model roles. Each gets a live footer row (current tool, tool count, elapsed); click it to open its transcript and report, or stop it. |
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
- | **Footer & themes** | Responsive footer (model, effort, session, cwd, context, RAM, cost, quota, goal, roles, branch) and seven dark themes. |
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
 
32
32
  ## Install
33
33
 
@@ -64,12 +64,12 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
64
64
  | `/jar tasks [list\|add\|done\|open\|edit\|delete]` | Human view/editor for the agent's checklist. |
65
65
  | `/jar history` | Read-only conversation timeline for the active branch. |
66
66
  | `/diff` | Review, accept or revert the files the agent changed. |
67
- | `/jar shells` | Background shells: live output, kill. |
67
+ | `/jar activity` · `/jar shells` | Subagents and background shells: live transcript/output, stop or kill. |
68
68
  | `/jar resume [N]` | Resume recent session `N` from the welcome list, or pick one. |
69
- | `/jar sessions [query]` · `/jar name <title>` | Search/switch sessions; name the current one. |
69
+ | `/jar sessions [query]` · `/jar name <title>` | Search sessions with a details pane (prompt, messages, goal, plan) and switch; name the current one. |
70
70
  | `/jar welcome` | Replay the welcome screen. |
71
71
  | `/jar ask [question]` | Answer a question in a dialog and insert the answer into the editor. |
72
- | `/jar accent [preset]` · `/jar footer` | Switch a loaded accent theme; toggle footer fields. |
72
+ | `/jar accent [preset]` · `/jar footer` · `/jar icons [unicode\|nerd\|ascii]` | Switch a loaded accent theme; toggle footer fields; pick the icon set. |
73
73
  | `/jar composer on\|off` · `/jar animations on\|off` · `/jar ui on\|off` | Toggle the composer, motion, or all pi-jar UI. |
74
74
  | `/jar quota on\|off` | Session-only, read-only quota lookups for supported OAuth providers. |
75
75
  | `/jar hub` | Open an installed task or subagent manager command. |
@@ -78,6 +78,7 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
78
78
  | Shortcut | Action |
79
79
  | --- | --- |
80
80
  | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> | Open pi-jar settings |
81
+ | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd> | Subagents and background shells (activity view) |
81
82
  | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> | Refresh the welcome (new message and flame), or show it again |
82
83
  | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>P</kbd> | Toggle plan mode |
83
84
  | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>M</kbd> | Cycle model roles (`cycleOrder`) |
@@ -219,8 +220,9 @@ The advisor is a second model (the `advisor` role; the current model if unassign
219
220
  - **`jar_todo`** — a Claude-style task list the agent keeps for any multi-step request. It writes the full list at once; each task is `pending`, `in_progress` (exactly one at a time) or `completed`, with an `activeForm` ("Running tests") that replaces the working spinner text while it runs. The live checklist above the composer shows `✔` struck-through done tasks, a bold `◼` current task and `☐` pending ones; a finished list stays until your next prompt. `/jar tasks` is your view/editor: `a` add, <kbd>Space</kbd>/<kbd>Enter</kbd> check, `e` edit, `d` delete, `f` filter.
220
221
  - **`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.
221
222
  - **`/diff`** — review what the agent changed since its 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. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
222
- - **`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). The footer shows `⚙ N shells`; `/jar shells` opens a live view (`x` kill, `f` follow). All shells stop when the session ends.
223
- - **`jar_delegate`** — up to four subagents run in parallel as separate one-shot Pi processes with a fresh context, on a role (default `task`, then `default`, then the current model). They are read-only (`read`, `grep`, `find`, `ls`) unless `write: true`; subagents cannot delegate further; aborting the turn stops them. Each shows as a working teammate until it finishes, and the tool result lists every report.
223
+ - **`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 four subagents run in parallel as separate one-shot Pi processes with a fresh context, on a role (default `task`, then `default`, then the current model). They are read-only (`read`, `grep`, `find`, `ls`) unless `write: true`; subagents cannot delegate further; aborting the turn stops them. Each has a live footer row; the activity view (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>, `/jar activity`, a click on the row) shows its task, live transcript and report, and `x` stops just that one. The tool result lists every report.
225
+ - **Activity view** — one split view 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). Finished runs stay listed for review.
224
226
  - **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
225
227
  - 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.
226
228
 
@@ -228,8 +230,8 @@ The advisor is a second model (the `advisor` role; the current model if unassign
228
230
 
229
231
  `/jar settings` (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd>, or the welcome's Settings action) has three tabs:
230
232
 
231
- - **Appearance** — accent, motion, rounded composer, Ember mascot, next-prompt suggestions, pi-jar UI.
232
- - **Footer** — field visibility.
233
+ - **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
+ - **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).
233
235
  - **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, advisor, advisor gates.
234
236
 
235
237
  pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
@@ -258,8 +260,10 @@ pi-jar/
258
260
  │ ├── usage-view.ts /usage; context-view.ts is /context; panel.ts frames both
259
261
  │ ├── split-view.ts shared two-pane frame
260
262
  │ ├── changes.ts change tracker and line diff; diff-view.ts is /diff
261
- │ ├── shells.ts background shells, jar_shell and /jar shells
262
- │ ├── delegate.ts jar_delegate subagents
263
+ │ ├── shells.ts background shells and jar_shell
264
+ │ ├── delegate.ts jar_delegate subagents and their live registry
265
+ │ ├── activity-view.ts subagent/shell details view
266
+ │ ├── icons.ts unicode / nerd / ascii glyph sets
263
267
  │ ├── attachments.ts image chips; prompt-search.ts is prompt history
264
268
  │ ├── session-gallery.ts recent sessions for the welcome
265
269
  │ ├── welcome.ts welcome layout and hit-testing
@@ -56,11 +56,13 @@ Then check:
56
56
 
57
57
  - the welcome at 24, 40, 80 and 120+ columns; <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> refresh; motion off (`/jar animations off`) freezes the flame;
58
58
  - the composer: typing `/pl` shows autocomplete below the frame; long drafts grow before scrolling; Ember changes mood while dialogs are open;
59
- - `/jar settings`: Tab through Appearance → Footer → Pi, toggle mouse mode and check Pi's `settings.json` in the temporary agent dir;
59
+ - `/jar settings`: Tab through Appearance → Footer → Pi; cycle Unicode/Nerd Font/ASCII icons and toggle mouse mode in the temporary agent dir;
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;
60
62
  - `/roles`: the split manager, `n` new role, `a` alias, `s` scope;
61
63
  - `/plan`: the status shows `◆ PLAN · read-only` and a directory appears under `$TMPDIR/pi-jar/plans/`.
62
64
 
63
- For pointer interaction start with `--tui-mode fullscreen` (or enable **Pi → Mouse clicks** and restart). Check that welcome actions fire once per press, that clicking composer text moves the cursor, that clicking Ember pokes it, that plan-view headings and actions respond to clicks and the wheel, and that selecting text anywhere still copies it.
65
+ For pointer interaction start with `--tui-mode fullscreen` (or enable **Pi → Mouse clicks** and restart). Check that welcome actions fire once per press, that footer activity rows open their matching details, that the footer session indicator opens search, that clicking composer text moves the cursor, that clicking Ember pokes it, that plan-view headings and actions respond to clicks and the wheel, and that selecting text anywhere still copies it. In regular mode use `/jar activity` and `/jar sessions` instead.
64
66
 
65
67
  With a logged-in model, run end to end:
66
68
 
package/docs/WORKFLOWS.md CHANGED
@@ -132,7 +132,8 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
132
132
  - `jar_delegate` takes 1–4 tasks. Each runs `pi --mode json -p --no-session --model <role model> [--thinking <effort>] [--tools read,grep,find,ls] "<task>"` in the project directory, with `PI_JAR_CHILD=1`.
133
133
  - The model comes from the task's role (default `task`), falling back to `default` and then the current model.
134
134
  - Progress (tools used, turns, cost) streams into the tool card; each running subagent is published as a teammate for the welcome TEAM row and footer, and cleared when it ends.
135
- - Subagents time out after 20 minutes and stop when the turn is aborted. The result lists every report with its role, model and outcome. In goal mode, `write: true` needs an open task.
135
+ - The activity view reads a live transcript per subagent (each tool call with its command, path or pattern, then the assistant's text; last 200 lines). It lives only in memory with the 8 most recent finished runs; the tool result saved in the session carries the reports, not the transcript.
136
+ - Subagents time out after 20 minutes and stop when the turn is aborted; `x` in the activity view stops one without touching the rest of its batch (its report reads `failed: stopped`). The result lists every report with its role, model and outcome. In goal mode, `write: true` needs an open task.
136
137
 
137
138
  ## Sessions and prompt history
138
139
 
@@ -1,4 +1,4 @@
1
- import { getAgentDir, SessionManager, SettingsManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import { getAgentDir, SessionManager, SettingsManager, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { basename } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { existsSync, readFileSync } from "node:fs";
@@ -9,7 +9,7 @@ import { ComposerStyle } from "../src/composer.ts";
9
9
  import { installCompactBuiltinTools } from "../src/compact-tools.ts";
10
10
  import { WELCOME_INTERVAL_MS } from "../src/animations.ts";
11
11
  import { promptText } from "../src/dialogs.ts";
12
- import { renderFooter } from "../src/footer.ts";
12
+ import { renderFooterLayout, type ActivityState, type FooterActivity, type FooterHit, type FooterTarget } from "../src/footer.ts";
13
13
  import { openJarHistory } from "../src/history-ui.ts";
14
14
  import { GOAL_ENTRY, GoalStore } from "../src/goals.ts";
15
15
  import { GoalLoop } from "../src/goal-loop.ts";
@@ -27,19 +27,21 @@ import { PlanMode } from "../src/plan.ts";
27
27
  import { openRolesUi } from "../src/roles-ui.ts";
28
28
  import { registerSuggestions, SuggestionState } from "../src/suggest.ts";
29
29
  import { createDemoRoles } from "../src/roles.ts";
30
- import { ACTIVE_STATES, collectStatuses, type JarRole } from "../src/status.ts";
30
+ import { ACTIVE_STATES, cleanText, collectStatuses, type JarRole } from "../src/status.ts";
31
31
  import { manageTasks } from "../src/tasks-ui.ts";
32
32
  import { registerTaskTool, todoRow } from "../src/task-tool.ts";
33
33
  import { TASK_ENTRY, TodoStore } from "../src/tasks.ts";
34
34
  import { formatCost, sessionCost } from "../src/usage.ts";
35
35
  import { WorkingState } from "../src/working.ts";
36
36
  import { ChangeTracker } from "../src/changes.ts";
37
- import { registerDelegate } from "../src/delegate.ts";
37
+ import { DelegateRegistry, registerDelegate, type DelegateRun } from "../src/delegate.ts";
38
+ import { openActivityView, type ActivityTarget } from "../src/activity-view.ts";
39
+ import { ICON_SETS, setIconSet, withIcon } from "../src/icons.ts";
38
40
  import { collectPrompts, openPromptSearch } from "../src/prompt-search.ts";
39
- import { ago, recentSessions, type RecentSession } from "../src/session-gallery.ts";
41
+ import { ago, recentSessions, sessionDetails, type RecentSession } from "../src/session-gallery.ts";
40
42
  import { clearSessionBranchCache, sessionBranch } from "../src/session-branch.ts";
41
43
  import { registerChangeReview } from "../src/diff-view.ts";
42
- import { openShellsView, registerShells, SHELL_MESSAGE, shellEventMessage, ShellManager, type ShellEvent } from "../src/shells.ts";
44
+ import { registerShells, SHELL_MESSAGE, shellEventMessage, ShellManager, type ShellEvent, type ShellJob } from "../src/shells.ts";
43
45
  import { hopefulWelcomeMessage, welcomeHit, welcomeLines, type WelcomeAction } from "../src/welcome.ts";
44
46
 
45
47
  const WELCOME_KEY = "pi-jar.welcome";
@@ -50,6 +52,8 @@ const VERSION = (() => {
50
52
  /** Ignore the click Pi may synthesize right after a press we already acted on. */
51
53
  const WELCOME_CLICK_DEDUPE_MS = 400;
52
54
  const LARGE_SESSION_ENTRIES = 800;
55
+ /** Entries Pi writes when a fresh session starts; a branch holding only these has no transcript yet. */
56
+ const SESSION_SETUP_ENTRIES = new Set(["thinking_level_change", "model_change", "session_info"]);
53
57
  export default function piJar(pi: ExtensionAPI): void {
54
58
  installCompactBuiltinTools(pi);
55
59
  let demo = false;
@@ -131,12 +135,71 @@ export default function piJar(pi: ExtensionAPI): void {
131
135
  { triggerTurn: true, deliverAs: "followUp" });
132
136
  } catch (error) { console.error("pi-jar: could not deliver shell event", error); }
133
137
  };
134
- /** Footer indicators owned by pi-jar. */
135
- const footerChips = () => {
136
- const running = shells?.running() ?? 0;
137
- return [...(changeCount ? [`± ${changeCount} file${changeCount === 1 ? "" : "s"} · /diff`] : []),
138
- ...(running ? [`⚙ ${running} shell${running === 1 ? "" : "s"}`] : [])];
138
+ /** Footer indicators owned by pi-jar; live subagents and shells get their own rows. */
139
+ const footerChips = () => changeCount ? [withIcon("changes", `${changeCount} file${changeCount === 1 ? "" : "s"} · /diff`)] : [];
140
+ const subagents = new DelegateRegistry();
141
+ subagents.subscribe(() => footerTui?.requestRender());
142
+ /** Finished work stays visible briefly so a quick run is not a flicker. */
143
+ const ACTIVITY_LINGER_MS = 5000;
144
+ const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
145
+ : run.state === "done" ? "done" : run.error === "stopped" ? "stopped" : "failed";
146
+ const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
147
+ : job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
148
+ 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
+ const footerActivity = (now: number): FooterActivity[] => {
150
+ const rows: FooterActivity[] = [];
151
+ for (const { key, run } of subagents.records()) {
152
+ if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
153
+ rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
154
+ detail: run.error ?? run.activity ?? cleanText(run.task, 120),
155
+ stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
156
+ }
157
+ for (const job of shells?.summaries() ?? []) {
158
+ if (job.endedAt && now - job.endedAt > ACTIVITY_LINGER_MS) continue;
159
+ // Shell output does not repaint the footer, so show stable facts rather than a stale clock.
160
+ rows.push({ kind: "shell", id: job.id, name: `${job.id} ${job.name}`, state: shellActivityState(job), detail: job.command,
161
+ ...(job.watch ? { stats: `watch /${cleanText(job.watch, 20)}/` + (job.matched ? " ✓" : "") } : {}) });
162
+ }
163
+ return rows;
164
+ };
165
+ let overlayOpen = false;
166
+ /** Subagents, shells and other extensions' roles in one live split view. */
167
+ const openActivity = async (ctx: ExtensionContext, initial?: ActivityTarget) => {
168
+ if (!ctx.hasUI || ctx.mode !== "tui" || overlayOpen) return;
169
+ overlayOpen = true;
170
+ try {
171
+ await openActivityView(ctx, {
172
+ subagents, ...(shells ? { shells } : {}),
173
+ roles: () => collectStatuses(welcomeStatuses(), Date.now()).roles.filter((role) => !role.id.startsWith("delegate-"))
174
+ .map((role) => ({ id: role.id, name: role.name, state: role.state, ...(role.task ? { task: role.task } : {}) }))
175
+ }, initial);
176
+ } finally { overlayOpen = false; }
177
+ };
178
+ /** Searchable project sessions with a details pane; Enter resumes the selected one. */
179
+ const openSessions = async (ctx: ExtensionContext | ExtensionCommandContext, query = "") => {
180
+ if (!ctx.hasUI || ctx.mode !== "tui") { ctx.ui.notify("Session search requires the interactive TUI", "warning"); return; }
181
+ if (overlayOpen) return;
182
+ let sessions;
183
+ try { sessions = (await SessionManager.list(ctx.cwd)).sort((a, b) => b.modified.getTime() - a.modified.getTime()); }
184
+ catch (error) { ctx.ui.notify("Could not search sessions: " + String(error), "error"); return; }
185
+ if (!sessions.length) { ctx.ui.notify("No sessions found for this project", "info"); return; }
186
+ overlayOpen = true;
187
+ let selected: string | undefined;
188
+ try { selected = await pickSession(ctx, sessions, query, (session) => sessionDetails(session.path)); }
189
+ finally { overlayOpen = false; }
190
+ if (!selected || selected === ctx.sessionManager.getSessionFile()) return;
191
+ if (!("switchSession" in ctx)) {
192
+ // Clicks and shortcuts get a plain context; switching needs a command, so stage it.
193
+ ctx.ui.setEditorText(`/jar resume ${selected}`);
194
+ ctx.ui.notify("Press Enter to resume that session", "info");
195
+ return;
196
+ }
197
+ // Never access the old command context after a successful switch.
198
+ const result = await ctx.switchSession(selected);
199
+ if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
139
200
  };
201
+ const openFooterTarget = (ctx: ExtensionContext, target: FooterTarget) => target.kind === "session" ? openSessions(ctx)
202
+ : openActivity(ctx, target.kind === "activity" ? undefined : target);
140
203
  registerAskTool(pi);
141
204
  const modelRoles = new ModelRoleManager(pi);
142
205
  const sideUsage = new SideUsage();
@@ -144,7 +207,7 @@ export default function piJar(pi: ExtensionAPI): void {
144
207
  registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
145
208
  quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
146
209
  modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
147
- registerDelegate(pi, modelRoles);
210
+ registerDelegate(pi, modelRoles, subagents);
148
211
  const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
149
212
  planMode.register();
150
213
  const goalLoop = new GoalLoop(pi, {
@@ -360,6 +423,8 @@ export default function piJar(pi: ExtensionAPI): void {
360
423
  let context = "ctx ?";
361
424
  let contextSampledAt = 0;
362
425
  let disposed = false;
426
+ let hits: FooterHit[] = [];
427
+ let pressed: { target: FooterTarget; at: number } | undefined;
363
428
  const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
364
429
  const dispose = () => {
365
430
  if (disposed) return;
@@ -374,12 +439,26 @@ export default function piJar(pi: ExtensionAPI): void {
374
439
  return {
375
440
  dispose,
376
441
  invalidate() {},
442
+ handleMouse(event: TuiMouseEvent) {
443
+ if (event.button !== "left") return;
444
+ const hit = hits.find((item) => item.y === event.y && event.x >= item.x0 && event.x < item.x1);
445
+ if (!hit) return;
446
+ if (event.type !== "press" && event.type !== "click") return { handled: true };
447
+ // Act on press (click synthesis is not guaranteed through multiplexers); drop the echo click.
448
+ const now = Date.now();
449
+ const same = pressed && JSON.stringify(pressed.target) === JSON.stringify(hit.target);
450
+ if (event.type === "click" && same && now - pressed!.at < WELCOME_CLICK_DEDUPE_MS) return { handled: true };
451
+ pressed = { target: hit.target, at: now };
452
+ queueMicrotask(() => { void openFooterTarget(ctx, hit.target).catch((error) => ctx.ui.notify("pi-jar: " + String(error), "error")); });
453
+ return { handled: true, capture: event.type === "press" };
454
+ },
377
455
  render(width: number): string[] {
378
456
  try {
379
457
  const now = Date.now();
380
458
  const statuses = footerData.getExtensionStatuses();
381
459
  const live = collectStatuses(statuses, now);
382
- const roles: JarRole[] = demo ? createDemoRoles() : live.roles;
460
+ // jar_delegate runs get their own activity rows; other extensions' roles stay as chips.
461
+ const roles: JarRole[] = demo ? createDemoRoles() : live.roles.filter((role) => !role.id.startsWith("delegate-"));
383
462
  const nearest = demo ? undefined : roles.reduce<number | undefined>((min, role) =>
384
463
  role.expiresAt != null ? Math.min(min ?? Infinity, role.expiresAt) : min, undefined);
385
464
  if (nearest !== expiryAt) {
@@ -406,15 +485,18 @@ export default function piJar(pi: ExtensionAPI): void {
406
485
  memorySampledAt = now;
407
486
  }
408
487
  const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, now) : undefined;
409
- return renderFooter({
488
+ const layout = renderFooterLayout({
410
489
  model: ctx.model?.id ?? "no-model", effort: ctx.model?.reasoning === false ? "off" : (pi.getThinkingLevel?.() ?? "off"),
411
490
  sessionName: ctx.sessionManager?.getSessionName?.(),
412
491
  cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
413
- context, goal: goalLoop.progress(), chips: footerChips(),
492
+ context, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
414
493
  memory, cost: formatCost(cost), quota, roles, extras: live.extras,
415
494
  demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
416
495
  }, width, ctx.ui.theme ?? theme);
496
+ hits = layout.hits;
497
+ return layout.lines;
417
498
  } catch {
499
+ hits = [];
418
500
  if (expiryTimer) clearTimeout(expiryTimer);
419
501
  expiryTimer = undefined;
420
502
  expiryAt = undefined;
@@ -438,6 +520,7 @@ export default function piJar(pi: ExtensionAPI): void {
438
520
  return;
439
521
  }
440
522
  visualSettings = next;
523
+ setIconSet(next.icons);
441
524
  enabled = next.ui;
442
525
  animations = next.animations;
443
526
  if (footerSettings.quota && !next.footer.quota) quotaCache?.stop();
@@ -492,6 +575,7 @@ export default function piJar(pi: ExtensionAPI): void {
492
575
  sideUsage.clear();
493
576
  migrateLegacySettings(getAgentDir());
494
577
  visualSettings = loadVisualSettings(getAgentDir());
578
+ setIconSet(visualSettings.icons);
495
579
  animations = visualSettings.animations;
496
580
  enabled = visualSettings.ui;
497
581
  footerSettings = visualSettings.footer;
@@ -504,6 +588,7 @@ export default function piJar(pi: ExtensionAPI): void {
504
588
  changes = new ChangeTracker(() => ctx.cwd);
505
589
  changeCount = 0;
506
590
  shells?.dispose();
591
+ subagents.clear();
507
592
  shells = new ShellManager(onShellEvent);
508
593
  shells.onChange = () => footerTui?.requestRender();
509
594
  goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
@@ -538,7 +623,10 @@ export default function piJar(pi: ExtensionAPI): void {
538
623
  }
539
624
  // Resumed sessions already have a transcript to paint. Rebuilding the animated welcome and
540
625
  // scanning other session files competes with that expensive initial render for no real benefit.
541
- if (branch.length === 0) showWelcome(ctx);
626
+ // A fresh session is not empty: Pi records the startup thinking level/model as entries.
627
+ const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
628
+ && typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
629
+ if (!resumed) showWelcome(ctx);
542
630
  });
543
631
  // Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
544
632
  const dismissWelcome = (ctx: ExtensionContext) => {
@@ -602,6 +690,7 @@ export default function piJar(pi: ExtensionAPI): void {
602
690
  changeCount = 0;
603
691
  shells?.dispose();
604
692
  shells = undefined;
693
+ subagents.clear();
605
694
  goals = undefined;
606
695
  disposeFooter?.();
607
696
  disposeFooter = undefined;
@@ -633,6 +722,10 @@ export default function piJar(pi: ExtensionAPI): void {
633
722
  description: "Cycle pi-jar model roles",
634
723
  handler: async (ctx) => { await modelRoles.cycle(ctx); footerTui?.requestRender(); }
635
724
  });
725
+ pi.registerShortcut?.(Key.ctrlAlt("a"), {
726
+ description: "Subagents and background shells (pi-jar)",
727
+ handler: async (ctx) => { await openActivity(ctx); }
728
+ });
636
729
  pi.registerShortcut?.(Key.ctrlAlt("s"), {
637
730
  description: "Open pi-jar settings",
638
731
  handler: async (ctx) => {
@@ -656,25 +749,24 @@ export default function piJar(pi: ExtensionAPI): void {
656
749
  }
657
750
  // `/jar resume` without a number opens the same searchable picker.
658
751
  if (command === "sessions" || command.startsWith("sessions ") || command === "resume") {
659
- if (!ctx.hasUI || ctx.mode !== "tui") { ctx.ui.notify("Session search requires the interactive TUI", "warning"); return; }
660
- let sessions;
661
- try { sessions = (await SessionManager.list(ctx.cwd)).sort((a, b) => b.modified.getTime() - a.modified.getTime()); }
662
- catch (error) { ctx.ui.notify("Could not search sessions: " + String(error), "error"); return; }
663
- if (!sessions.length) { ctx.ui.notify("No sessions found for this project", "info"); return; }
664
- const selected = await pickSession(ctx, sessions, command === "resume" ? "" : args.trim().slice(8).trim());
665
- if (selected && selected !== ctx.sessionManager.getSessionFile()) {
666
- // Never access the old command context after a successful switch.
667
- const result = await ctx.switchSession(selected);
668
- if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
669
- }
752
+ await openSessions(ctx, command === "resume" ? "" : args.trim().slice(8).trim());
670
753
  return;
671
754
  }
672
755
  if (command.startsWith("resume ")) {
673
- const index = Number(command.slice(7).trim());
756
+ const arg = args.trim().slice(7).trim();
757
+ const index = Number(arg);
674
758
  let recent = welcomeRecent;
675
- try { if (!recent.length) recent = await loadRecent(ctx); } catch (error) { ctx.ui.notify("Could not list sessions: " + String(error), "error"); return; }
676
- const target = Number.isInteger(index) && index >= 1 ? recent[index - 1] : undefined;
677
- if (!target) { ctx.ui.notify(`No recent session ${command.slice(7).trim()}; try /jar sessions`, "warning"); return; }
759
+ let target: { path: string } | undefined;
760
+ try {
761
+ if (arg.endsWith(".jsonl")) {
762
+ // Clicks stage a path in the editor; restrict it to Pi's sessions for this project.
763
+ target = (await SessionManager.list(ctx.cwd)).find((session) => session.path === arg);
764
+ } else {
765
+ if (!recent.length) recent = await loadRecent(ctx);
766
+ target = Number.isInteger(index) && index >= 1 ? recent[index - 1] : undefined;
767
+ }
768
+ } catch (error) { ctx.ui.notify("Could not list sessions: " + String(error), "error"); return; }
769
+ if (!target) { ctx.ui.notify(`No recent session ${arg}; try /jar sessions`, "warning"); return; }
678
770
  const result = await ctx.switchSession(target.path);
679
771
  if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
680
772
  return;
@@ -769,10 +861,23 @@ export default function piJar(pi: ExtensionAPI): void {
769
861
  }
770
862
  if (command === "welcome") { showWelcome(ctx); return; }
771
863
  if (command === "commit" || command.startsWith("commit ")) { await jarCommit(pi, ctx, modelRoles, sideUsage, args.trim().slice(6).trim()); return; }
772
- if (command === "shells") {
773
- if (!shells) return;
774
- if (!ctx.hasUI || ctx.mode !== "tui") { ctx.ui.notify(shells.summaries().map((job) => `${job.id} ${job.name} ${job.status}`).join("\n") || "No background shells", "info"); return; }
775
- await openShellsView(ctx, shells);
864
+ if (command === "shells" || command === "activity" || command === "agents") {
865
+ if (!ctx.hasUI || ctx.mode !== "tui") {
866
+ const lines = [...subagents.records().map(({ run }) => `${run.name} ${run.state}`), ...(shells?.summaries() ?? []).map((job) => `${job.id} ${job.name} ${job.status}`)];
867
+ ctx.ui.notify(lines.join("\n") || "Nothing running", "info");
868
+ return;
869
+ }
870
+ const shell = command === "shells" ? shells?.summaries().find((job) => job.status === "running") ?? shells?.summaries()[0] : undefined;
871
+ await openActivity(ctx, shell ? { kind: "shell", id: shell.id } : undefined);
872
+ return;
873
+ }
874
+ if (command === "icons" || command.startsWith("icons ")) {
875
+ const requested = command.slice(5).trim();
876
+ const next = ICON_SETS.find((set) => set === requested)
877
+ ?? (requested ? undefined : ICON_SETS[(ICON_SETS.indexOf(visualSettings.icons) + 1) % ICON_SETS.length]);
878
+ if (!next) { ctx.ui.notify(`Icons: ${ICON_SETS.join(" | ")} (nerd needs a Nerd Font)`, "error"); return; }
879
+ applyVisualSettings({ ...visualSettings, icons: next }, ctx);
880
+ ctx.ui.notify(`pi-jar icons: ${next}`, "info");
776
881
  return;
777
882
  }
778
883
  if (command === "demo") demo = true;
@@ -784,7 +889,7 @@ export default function piJar(pi: ExtensionAPI): void {
784
889
  else if (command === "quota on" && quotaCache) { quotaCache.enabled = true; quotaDisabledByUser = false; }
785
890
  else if (command === "quota off" && quotaCache) { quotaCache.enabled = false; quotaDisabledByUser = true; quotaCache.stop(); }
786
891
  else {
787
- ctx.ui.notify("Usage: /jar [status|settings|commit [note]|sessions [search]|name <title>|history|footer|tasks|ask|composer on/off|accent [preset]|hub|welcome|demo|reset|animations on/off|ui on/off|quota on/off]", "error");
892
+ ctx.ui.notify("Usage: /jar [status|settings|activity|shells|sessions [search]|icons [unicode|nerd|ascii]|commit [note]|name <title>|history|footer|tasks|ask|composer on/off|accent [preset]|hub|welcome|demo|reset|animations on/off|ui on/off|quota on/off]", "error");
788
893
  return;
789
894
  }
790
895
  if (!["animations on", "animations off", "ui on", "ui off"].includes(command)) installUi(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jar",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",