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 +16 -12
- package/docs/DEVELOPMENT.md +4 -2
- package/docs/WORKFLOWS.md +2 -1
- package/extensions/index.ts +141 -36
- package/package.json +1 -1
- package/src/activity-view.ts +252 -0
- package/src/composer.ts +30 -9
- package/src/delegate.ts +150 -15
- package/src/footer.ts +105 -18
- package/src/icons.ts +57 -0
- package/src/plan.ts +1 -0
- package/src/prompt-search.ts +4 -2
- package/src/session-gallery.ts +8 -3
- package/src/session-ui.ts +106 -41
- package/src/settings-ui.ts +5 -2
- package/src/settings.ts +5 -1
- package/src/shells.ts +6 -83
- package/src/status.ts +2 -0
- package/src/welcome.ts +32 -12
- package/src/working.ts +3 -1
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.
|
|
28
|
-
| **Subagents** | `jar_delegate` fans out up to four read-only (or opt-in editing) subagents in parallel on your model roles
|
|
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` |
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
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
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
756
|
+
const arg = args.trim().slice(7).trim();
|
|
757
|
+
const index = Number(arg);
|
|
674
758
|
let recent = welcomeRecent;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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 (!
|
|
774
|
-
|
|
775
|
-
|
|
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|
|
|
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.
|
|
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",
|