pi-jar 0.1.2 → 0.1.4
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 +46 -16
- package/docs/DEVELOPMENT.md +4 -2
- package/docs/WORKFLOWS.md +7 -3
- package/extensions/index.ts +194 -71
- package/package.json +1 -1
- package/src/activity-view.ts +525 -0
- package/src/advisor.ts +9 -3
- package/src/changes.ts +35 -11
- package/src/child-baselines.ts +42 -0
- package/src/compact-tools.ts +58 -24
- package/src/composer.ts +69 -21
- package/src/delegate.ts +492 -51
- package/src/dialogs.ts +9 -5
- package/src/diff-view.ts +13 -1
- package/src/flame.ts +18 -5
- package/src/footer.ts +131 -26
- package/src/history-ui.ts +19 -12
- package/src/history.ts +3 -1
- package/src/icons.ts +57 -0
- package/src/model-roles.ts +62 -7
- package/src/panel.ts +5 -1
- package/src/plan.ts +1 -0
- package/src/prompt-search.ts +20 -10
- package/src/row-cache.ts +26 -0
- package/src/session-gallery.ts +8 -3
- package/src/session-ui.ts +112 -42
- package/src/settings-ui.ts +10 -11
- package/src/settings.ts +5 -1
- package/src/shells.ts +8 -84
- package/src/side-model.ts +45 -25
- package/src/split-view.ts +29 -12
- package/src/status.ts +2 -0
- package/src/task-tool.ts +140 -53
- package/src/tasks-ui.ts +15 -5
- package/src/tasks.ts +224 -38
- package/src/welcome.ts +47 -14
- package/src/working.ts +3 -1
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
|
-
import { TASK_ENTRY, TodoStore } from "../src/tasks.ts";
|
|
33
|
+
import { TASK_ENTRY, TodoStore, todoProgress, todoTotals } 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;
|
|
@@ -58,6 +62,17 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
58
62
|
let enabled = visualSettings.ui;
|
|
59
63
|
let disposeFooter: (() => void) | undefined;
|
|
60
64
|
let footerTui: { requestRender(): void } | undefined;
|
|
65
|
+
/** Footer facts that walk the session to compute; refreshed on the events that change them, never per frame. */
|
|
66
|
+
let footerContext = "ctx ?";
|
|
67
|
+
let footerSessionName: string | undefined;
|
|
68
|
+
const sampleFooter = (ctx: ExtensionContext) => {
|
|
69
|
+
try {
|
|
70
|
+
const usage = ctx.getContextUsage();
|
|
71
|
+
footerContext = usage?.percent == null || !Number.isFinite(usage.percent) ? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
|
|
72
|
+
} catch { footerContext = "ctx ?"; }
|
|
73
|
+
try { footerSessionName = ctx.sessionManager?.getSessionName?.(); } catch { footerSessionName = undefined; }
|
|
74
|
+
footerTui?.requestRender();
|
|
75
|
+
};
|
|
61
76
|
let quotaCache: QuotaCache | undefined;
|
|
62
77
|
let cost = 0;
|
|
63
78
|
let welcomeInterval: ReturnType<typeof setInterval> | undefined;
|
|
@@ -89,27 +104,37 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
89
104
|
if (open.length) todosAcknowledged = false;
|
|
90
105
|
const visible = enabled && items.length > 0 && (open.length > 0 || !todosAcknowledged);
|
|
91
106
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
// Rows are rebuilt only when the list changes (every change reinstalls the widget), the width or the theme.
|
|
108
|
+
ctx.ui.setWidget("pi-jar.todos", !visible ? undefined : (_tui, theme) => {
|
|
109
|
+
let memo: { width: number; sample: string; lines: string[] } | undefined;
|
|
110
|
+
return {
|
|
111
|
+
invalidate() { memo = undefined; },
|
|
112
|
+
render(width: number) {
|
|
113
|
+
const colors = ctx.ui.theme ?? theme;
|
|
114
|
+
const sample = colors.fg("accent", "·") + colors.fg("dim", "·");
|
|
115
|
+
if (memo && memo.width === width && memo.sample === sample) return memo.lines.slice();
|
|
116
|
+
const all = todos?.all() ?? [];
|
|
117
|
+
const { done, total } = todoTotals(all);
|
|
118
|
+
const parents = new Set<string>();
|
|
119
|
+
for (const item of all) if (item.parentId) parents.add(item.parentId);
|
|
120
|
+
// Show a window of up to 8 rows that keeps the running leaf (or next open one) in view.
|
|
121
|
+
const running = all.findIndex((item) => item.status === "in_progress" && !parents.has(item.id));
|
|
122
|
+
const focus = Math.max(0, running >= 0 ? running : all.findIndex((item) => !item.done && !parents.has(item.id)));
|
|
123
|
+
const start = Math.max(0, Math.min(focus - 2, all.length - 8));
|
|
124
|
+
const shown = all.slice(start, start + 8);
|
|
125
|
+
const rows = [
|
|
126
|
+
colors.fg("accent", "Tasks") + colors.fg("dim", ` · ${done}/${total} done` + (done === total ? " · all complete" : "") + " · /jar tasks"),
|
|
127
|
+
...(start > 0 ? [colors.fg("dim", ` … ${start} earlier`)] : []),
|
|
128
|
+
...shown.map((item) => todoRow(item, (color, text) => colors.fg(color, text), (text) => colors.bold(text), parents.has(item.id) ? todoProgress(all, item.id) : undefined)),
|
|
129
|
+
...(start + shown.length < all.length ? [colors.fg("dim", ` … +${all.length - start - shown.length} more`)] : [])
|
|
130
|
+
];
|
|
131
|
+
// Pi inserts a spacer before widgets, but not between widgets and the composer.
|
|
132
|
+
const lines = [...rows.map((line) => truncateToWidth(line, Math.max(0, width))), truncateToWidth(" ", Math.max(0, width))];
|
|
133
|
+
memo = { width, sample, lines };
|
|
134
|
+
return lines.slice();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
});
|
|
113
138
|
} catch { /* Optional widget; task data remains available via /jar tasks and jar_todo. */ }
|
|
114
139
|
};
|
|
115
140
|
registerTaskTool(pi, () => todos, (ctx) => updateTaskWidget(ctx));
|
|
@@ -131,12 +156,71 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
131
156
|
{ triggerTurn: true, deliverAs: "followUp" });
|
|
132
157
|
} catch (error) { console.error("pi-jar: could not deliver shell event", error); }
|
|
133
158
|
};
|
|
134
|
-
/** Footer indicators owned by pi-jar. */
|
|
135
|
-
const footerChips = () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
/** Footer indicators owned by pi-jar; live subagents and shells get their own rows. */
|
|
160
|
+
const footerChips = () => changeCount ? [withIcon("changes", `${changeCount} file${changeCount === 1 ? "" : "s"} · /diff`)] : [];
|
|
161
|
+
const subagents = new DelegateRegistry();
|
|
162
|
+
subagents.subscribe(() => footerTui?.requestRender());
|
|
163
|
+
/** Finished work stays visible briefly so a quick run is not a flicker. */
|
|
164
|
+
const ACTIVITY_LINGER_MS = 5000;
|
|
165
|
+
const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
|
|
166
|
+
: run.state === "done" ? "done" : run.error === "stopped" ? "stopped" : "failed";
|
|
167
|
+
const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
|
|
168
|
+
: job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
|
|
169
|
+
const seconds = (ms: number) => { const s = Math.max(0, Math.round(ms / 1000)); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; };
|
|
170
|
+
const footerActivity = (now: number): FooterActivity[] => {
|
|
171
|
+
const rows: FooterActivity[] = [];
|
|
172
|
+
for (const { key, run } of subagents.records()) {
|
|
173
|
+
if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
174
|
+
rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
|
|
175
|
+
detail: run.error ?? run.activity ?? cleanText(run.task, 120),
|
|
176
|
+
stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
|
|
177
|
+
}
|
|
178
|
+
for (const job of shells?.summaries() ?? []) {
|
|
179
|
+
if (job.endedAt && now - job.endedAt > ACTIVITY_LINGER_MS) continue;
|
|
180
|
+
// Shell output does not repaint the footer, so show stable facts rather than a stale clock.
|
|
181
|
+
rows.push({ kind: "shell", id: job.id, name: `${job.id} ${job.name}`, state: shellActivityState(job), detail: job.command,
|
|
182
|
+
...(job.watch ? { stats: `watch /${cleanText(job.watch, 20)}/` + (job.matched ? " ✓" : "") } : {}) });
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
};
|
|
186
|
+
let overlayOpen = false;
|
|
187
|
+
/** Subagents, shells and other extensions' roles in one live split view. */
|
|
188
|
+
const openActivity = async (ctx: ExtensionContext, initial?: ActivityTarget) => {
|
|
189
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || overlayOpen) return;
|
|
190
|
+
overlayOpen = true;
|
|
191
|
+
try {
|
|
192
|
+
await openActivityView(ctx, {
|
|
193
|
+
subagents, ...(shells ? { shells } : {}),
|
|
194
|
+
roles: () => collectStatuses(welcomeStatuses(), Date.now()).roles.filter((role) => !role.id.startsWith("delegate-"))
|
|
195
|
+
.map((role) => ({ id: role.id, name: role.name, state: role.state, ...(role.task ? { task: role.task } : {}) }))
|
|
196
|
+
}, initial);
|
|
197
|
+
} finally { overlayOpen = false; }
|
|
139
198
|
};
|
|
199
|
+
/** Searchable project sessions with a details pane; Enter resumes the selected one. */
|
|
200
|
+
const openSessions = async (ctx: ExtensionContext | ExtensionCommandContext, query = "") => {
|
|
201
|
+
if (!ctx.hasUI || ctx.mode !== "tui") { ctx.ui.notify("Session search requires the interactive TUI", "warning"); return; }
|
|
202
|
+
if (overlayOpen) return;
|
|
203
|
+
let sessions;
|
|
204
|
+
try { sessions = (await SessionManager.list(ctx.cwd)).sort((a, b) => b.modified.getTime() - a.modified.getTime()); }
|
|
205
|
+
catch (error) { ctx.ui.notify("Could not search sessions: " + String(error), "error"); return; }
|
|
206
|
+
if (!sessions.length) { ctx.ui.notify("No sessions found for this project", "info"); return; }
|
|
207
|
+
overlayOpen = true;
|
|
208
|
+
let selected: string | undefined;
|
|
209
|
+
try { selected = await pickSession(ctx, sessions, query, (session) => sessionDetails(session.path)); }
|
|
210
|
+
finally { overlayOpen = false; }
|
|
211
|
+
if (!selected || selected === ctx.sessionManager.getSessionFile()) return;
|
|
212
|
+
if (!("switchSession" in ctx)) {
|
|
213
|
+
// Clicks and shortcuts get a plain context; switching needs a command, so stage it.
|
|
214
|
+
ctx.ui.setEditorText(`/jar resume ${selected}`);
|
|
215
|
+
ctx.ui.notify("Press Enter to resume that session", "info");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Never access the old command context after a successful switch.
|
|
219
|
+
const result = await ctx.switchSession(selected);
|
|
220
|
+
if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
|
|
221
|
+
};
|
|
222
|
+
const openFooterTarget = (ctx: ExtensionContext, target: FooterTarget) => target.kind === "session" ? openSessions(ctx)
|
|
223
|
+
: openActivity(ctx, target.kind === "activity" ? undefined : target);
|
|
140
224
|
registerAskTool(pi);
|
|
141
225
|
const modelRoles = new ModelRoleManager(pi);
|
|
142
226
|
const sideUsage = new SideUsage();
|
|
@@ -144,7 +228,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
144
228
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
145
229
|
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
|
|
146
230
|
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
|
|
147
|
-
registerDelegate(pi, modelRoles);
|
|
231
|
+
registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges });
|
|
148
232
|
const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
|
|
149
233
|
planMode.register();
|
|
150
234
|
const goalLoop = new GoalLoop(pi, {
|
|
@@ -357,9 +441,9 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
357
441
|
let expiryAt: number | undefined;
|
|
358
442
|
let memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
359
443
|
let memorySampledAt = 0;
|
|
360
|
-
let context = "ctx ?";
|
|
361
|
-
let contextSampledAt = 0;
|
|
362
444
|
let disposed = false;
|
|
445
|
+
let hits: FooterHit[] = [];
|
|
446
|
+
let pressed: { target: FooterTarget; at: number } | undefined;
|
|
363
447
|
const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
|
|
364
448
|
const dispose = () => {
|
|
365
449
|
if (disposed) return;
|
|
@@ -374,12 +458,26 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
374
458
|
return {
|
|
375
459
|
dispose,
|
|
376
460
|
invalidate() {},
|
|
461
|
+
handleMouse(event: TuiMouseEvent) {
|
|
462
|
+
if (event.button !== "left") return;
|
|
463
|
+
const hit = hits.find((item) => item.y === event.y && event.x >= item.x0 && event.x < item.x1);
|
|
464
|
+
if (!hit) return;
|
|
465
|
+
if (event.type !== "press" && event.type !== "click") return { handled: true };
|
|
466
|
+
// Act on press (click synthesis is not guaranteed through multiplexers); drop the echo click.
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
const same = pressed && JSON.stringify(pressed.target) === JSON.stringify(hit.target);
|
|
469
|
+
if (event.type === "click" && same && now - pressed!.at < WELCOME_CLICK_DEDUPE_MS) return { handled: true };
|
|
470
|
+
pressed = { target: hit.target, at: now };
|
|
471
|
+
queueMicrotask(() => { void openFooterTarget(ctx, hit.target).catch((error) => ctx.ui.notify("pi-jar: " + String(error), "error")); });
|
|
472
|
+
return { handled: true, capture: event.type === "press" };
|
|
473
|
+
},
|
|
377
474
|
render(width: number): string[] {
|
|
378
475
|
try {
|
|
379
476
|
const now = Date.now();
|
|
380
477
|
const statuses = footerData.getExtensionStatuses();
|
|
381
478
|
const live = collectStatuses(statuses, now);
|
|
382
|
-
|
|
479
|
+
// jar_delegate runs get their own activity rows; other extensions' roles stay as chips.
|
|
480
|
+
const roles: JarRole[] = demo ? createDemoRoles() : live.roles.filter((role) => !role.id.startsWith("delegate-"));
|
|
383
481
|
const nearest = demo ? undefined : roles.reduce<number | undefined>((min, role) =>
|
|
384
482
|
role.expiresAt != null ? Math.min(min ?? Infinity, role.expiresAt) : min, undefined);
|
|
385
483
|
if (nearest !== expiryAt) {
|
|
@@ -394,27 +492,25 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
394
492
|
// Do not schedule cosmetic footer repaints. Role state still updates on Pi's native
|
|
395
493
|
// stream/tool renders, which keeps long transcripts responsive.
|
|
396
494
|
const active = animations && footerSettings.roles && roles.some((role) => ACTIVE_STATES.has(role.state));
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
const usage = ctx.getContextUsage();
|
|
400
|
-
context = usage?.percent == null || !Number.isFinite(usage.percent)
|
|
401
|
-
? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
|
|
402
|
-
contextSampledAt = now;
|
|
403
|
-
}
|
|
495
|
+
// Role glyphs advance with time, not with the render rate, so the footer memo still hits between steps.
|
|
496
|
+
if (active) frame = Math.floor(now / 250);
|
|
404
497
|
if (now - memorySampledAt >= 5000) {
|
|
405
498
|
memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
406
499
|
memorySampledAt = now;
|
|
407
500
|
}
|
|
408
501
|
const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, now) : undefined;
|
|
409
|
-
|
|
502
|
+
const layout = renderFooterLayout({
|
|
410
503
|
model: ctx.model?.id ?? "no-model", effort: ctx.model?.reasoning === false ? "off" : (pi.getThinkingLevel?.() ?? "off"),
|
|
411
|
-
sessionName:
|
|
504
|
+
sessionName: footerSessionName,
|
|
412
505
|
cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
|
|
413
|
-
context, goal: goalLoop.progress(), chips: footerChips(),
|
|
506
|
+
context: footerContext, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
|
|
414
507
|
memory, cost: formatCost(cost), quota, roles, extras: live.extras,
|
|
415
508
|
demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
|
|
416
509
|
}, width, ctx.ui.theme ?? theme);
|
|
510
|
+
hits = layout.hits;
|
|
511
|
+
return layout.lines;
|
|
417
512
|
} catch {
|
|
513
|
+
hits = [];
|
|
418
514
|
if (expiryTimer) clearTimeout(expiryTimer);
|
|
419
515
|
expiryTimer = undefined;
|
|
420
516
|
expiryAt = undefined;
|
|
@@ -438,6 +534,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
438
534
|
return;
|
|
439
535
|
}
|
|
440
536
|
visualSettings = next;
|
|
537
|
+
setIconSet(next.icons);
|
|
441
538
|
enabled = next.ui;
|
|
442
539
|
animations = next.animations;
|
|
443
540
|
if (footerSettings.quota && !next.footer.quota) quotaCache?.stop();
|
|
@@ -492,6 +589,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
492
589
|
sideUsage.clear();
|
|
493
590
|
migrateLegacySettings(getAgentDir());
|
|
494
591
|
visualSettings = loadVisualSettings(getAgentDir());
|
|
592
|
+
setIconSet(visualSettings.icons);
|
|
495
593
|
animations = visualSettings.animations;
|
|
496
594
|
enabled = visualSettings.ui;
|
|
497
595
|
footerSettings = visualSettings.footer;
|
|
@@ -504,6 +602,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
504
602
|
changes = new ChangeTracker(() => ctx.cwd);
|
|
505
603
|
changeCount = 0;
|
|
506
604
|
shells?.dispose();
|
|
605
|
+
subagents.clear();
|
|
507
606
|
shells = new ShellManager(onShellEvent);
|
|
508
607
|
shells.onChange = () => footerTui?.requestRender();
|
|
509
608
|
goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
|
|
@@ -524,6 +623,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
524
623
|
quotaDisabledByUser = false;
|
|
525
624
|
updateCost(ctx);
|
|
526
625
|
installUi(ctx);
|
|
626
|
+
sampleFooter(ctx);
|
|
527
627
|
composer.setMascot(visualSettings.mascot);
|
|
528
628
|
if (visualSettings.composer && enabled) composer.enable(ctx);
|
|
529
629
|
suggest.sync();
|
|
@@ -538,7 +638,10 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
538
638
|
}
|
|
539
639
|
// Resumed sessions already have a transcript to paint. Rebuilding the animated welcome and
|
|
540
640
|
// scanning other session files competes with that expensive initial render for no real benefit.
|
|
541
|
-
|
|
641
|
+
// A fresh session is not empty: Pi records the startup thinking level/model as entries.
|
|
642
|
+
const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
|
|
643
|
+
&& typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
|
|
644
|
+
if (!resumed) showWelcome(ctx);
|
|
542
645
|
});
|
|
543
646
|
// Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
|
|
544
647
|
const dismissWelcome = (ctx: ExtensionContext) => {
|
|
@@ -561,6 +664,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
561
664
|
pi.on("agent_start", (_event, ctx) => { dismissWelcome(ctx); working.start(); applyWorking(ctx); });
|
|
562
665
|
pi.on("turn_start", (_event, ctx) => { working.start(); applyWorking(ctx); });
|
|
563
666
|
pi.on("message_end", (event, ctx) => {
|
|
667
|
+
// Every message (user, assistant, tool result) moves the context window.
|
|
668
|
+
sampleFooter(ctx);
|
|
564
669
|
if (event.message.role === "assistant") {
|
|
565
670
|
working.reportOutputTokens(event.message.usage?.output ?? 0);
|
|
566
671
|
addMessageCost(event.message);
|
|
@@ -583,11 +688,12 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
583
688
|
restoreGoal(ctx, branch);
|
|
584
689
|
updateCost(ctx);
|
|
585
690
|
composer.refreshSession(ctx);
|
|
691
|
+
sampleFooter(ctx);
|
|
586
692
|
});
|
|
587
|
-
pi.on("session_compact", (_event, ctx) => { restoreTodos(ctx); restoreGoal(ctx); updateCost(ctx); });
|
|
588
|
-
pi.on("model_select", (_event,
|
|
693
|
+
pi.on("session_compact", (_event, ctx) => { restoreTodos(ctx); restoreGoal(ctx); updateCost(ctx); sampleFooter(ctx); });
|
|
694
|
+
pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
|
|
589
695
|
pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
|
|
590
|
-
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx);
|
|
696
|
+
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
|
|
591
697
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
592
698
|
clearSessionBranchCache(ctx);
|
|
593
699
|
stopWelcome(ctx);
|
|
@@ -602,6 +708,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
602
708
|
changeCount = 0;
|
|
603
709
|
shells?.dispose();
|
|
604
710
|
shells = undefined;
|
|
711
|
+
subagents.clear();
|
|
605
712
|
goals = undefined;
|
|
606
713
|
disposeFooter?.();
|
|
607
714
|
disposeFooter = undefined;
|
|
@@ -633,6 +740,10 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
633
740
|
description: "Cycle pi-jar model roles",
|
|
634
741
|
handler: async (ctx) => { await modelRoles.cycle(ctx); footerTui?.requestRender(); }
|
|
635
742
|
});
|
|
743
|
+
pi.registerShortcut?.(Key.ctrlAlt("a"), {
|
|
744
|
+
description: "Subagents and background shells (pi-jar)",
|
|
745
|
+
handler: async (ctx) => { await openActivity(ctx); }
|
|
746
|
+
});
|
|
636
747
|
pi.registerShortcut?.(Key.ctrlAlt("s"), {
|
|
637
748
|
description: "Open pi-jar settings",
|
|
638
749
|
handler: async (ctx) => {
|
|
@@ -656,25 +767,24 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
656
767
|
}
|
|
657
768
|
// `/jar resume` without a number opens the same searchable picker.
|
|
658
769
|
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
|
-
}
|
|
770
|
+
await openSessions(ctx, command === "resume" ? "" : args.trim().slice(8).trim());
|
|
670
771
|
return;
|
|
671
772
|
}
|
|
672
773
|
if (command.startsWith("resume ")) {
|
|
673
|
-
const
|
|
774
|
+
const arg = args.trim().slice(7).trim();
|
|
775
|
+
const index = Number(arg);
|
|
674
776
|
let recent = welcomeRecent;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
777
|
+
let target: { path: string } | undefined;
|
|
778
|
+
try {
|
|
779
|
+
if (arg.endsWith(".jsonl")) {
|
|
780
|
+
// Clicks stage a path in the editor; restrict it to Pi's sessions for this project.
|
|
781
|
+
target = (await SessionManager.list(ctx.cwd)).find((session) => session.path === arg);
|
|
782
|
+
} else {
|
|
783
|
+
if (!recent.length) recent = await loadRecent(ctx);
|
|
784
|
+
target = Number.isInteger(index) && index >= 1 ? recent[index - 1] : undefined;
|
|
785
|
+
}
|
|
786
|
+
} catch (error) { ctx.ui.notify("Could not list sessions: " + String(error), "error"); return; }
|
|
787
|
+
if (!target) { ctx.ui.notify(`No recent session ${arg}; try /jar sessions`, "warning"); return; }
|
|
678
788
|
const result = await ctx.switchSession(target.path);
|
|
679
789
|
if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
|
|
680
790
|
return;
|
|
@@ -769,10 +879,23 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
769
879
|
}
|
|
770
880
|
if (command === "welcome") { showWelcome(ctx); return; }
|
|
771
881
|
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
|
-
|
|
882
|
+
if (command === "shells" || command === "activity" || command === "agents") {
|
|
883
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
884
|
+
const lines = [...subagents.records().map(({ run }) => `${run.name} ${run.state}`), ...(shells?.summaries() ?? []).map((job) => `${job.id} ${job.name} ${job.status}`)];
|
|
885
|
+
ctx.ui.notify(lines.join("\n") || "Nothing running", "info");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const shell = command === "shells" ? shells?.summaries().find((job) => job.status === "running") ?? shells?.summaries()[0] : undefined;
|
|
889
|
+
await openActivity(ctx, shell ? { kind: "shell", id: shell.id } : undefined);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (command === "icons" || command.startsWith("icons ")) {
|
|
893
|
+
const requested = command.slice(5).trim();
|
|
894
|
+
const next = ICON_SETS.find((set) => set === requested)
|
|
895
|
+
?? (requested ? undefined : ICON_SETS[(ICON_SETS.indexOf(visualSettings.icons) + 1) % ICON_SETS.length]);
|
|
896
|
+
if (!next) { ctx.ui.notify(`Icons: ${ICON_SETS.join(" | ")} (nerd needs a Nerd Font)`, "error"); return; }
|
|
897
|
+
applyVisualSettings({ ...visualSettings, icons: next }, ctx);
|
|
898
|
+
ctx.ui.notify(`pi-jar icons: ${next}`, "info");
|
|
776
899
|
return;
|
|
777
900
|
}
|
|
778
901
|
if (command === "demo") demo = true;
|
|
@@ -784,7 +907,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
784
907
|
else if (command === "quota on" && quotaCache) { quotaCache.enabled = true; quotaDisabledByUser = false; }
|
|
785
908
|
else if (command === "quota off" && quotaCache) { quotaCache.enabled = false; quotaDisabledByUser = true; quotaCache.stop(); }
|
|
786
909
|
else {
|
|
787
|
-
ctx.ui.notify("Usage: /jar [status|settings|
|
|
910
|
+
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
911
|
return;
|
|
789
912
|
}
|
|
790
913
|
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.4",
|
|
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",
|