pi-jar 0.1.1 → 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 +212 -89
- package/package.json +1 -1
- package/src/activity-view.ts +252 -0
- package/src/advisor.ts +30 -6
- package/src/changes.ts +12 -20
- package/src/compact-tools.ts +62 -18
- package/src/composer.ts +36 -14
- package/src/delegate.ts +187 -23
- package/src/diff-view.ts +2 -2
- package/src/footer.ts +105 -18
- package/src/goal-loop.ts +34 -9
- package/src/goals.ts +13 -2
- package/src/history-ui.ts +2 -1
- package/src/icons.ts +57 -0
- package/src/info-panels.ts +19 -7
- package/src/plan.ts +37 -10
- package/src/prompt-search.ts +27 -8
- package/src/quota.ts +14 -4
- package/src/session-branch.ts +24 -0
- package/src/session-gallery.ts +10 -5
- package/src/session-ui.ts +119 -45
- package/src/settings-ui.ts +5 -2
- package/src/settings.ts +5 -1
- package/src/shells.ts +77 -99
- package/src/side-model.ts +14 -2
- package/src/status.ts +36 -24
- package/src/suggest.ts +6 -25
- package/src/task-tool.ts +35 -11
- package/src/tasks.ts +24 -7
- package/src/usage.ts +2 -1
- package/src/welcome.ts +32 -12
- 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,18 +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";
|
|
42
|
+
import { clearSessionBranchCache, sessionBranch } from "../src/session-branch.ts";
|
|
40
43
|
import { registerChangeReview } from "../src/diff-view.ts";
|
|
41
|
-
import {
|
|
44
|
+
import { registerShells, SHELL_MESSAGE, shellEventMessage, ShellManager, type ShellEvent, type ShellJob } from "../src/shells.ts";
|
|
42
45
|
import { hopefulWelcomeMessage, welcomeHit, welcomeLines, type WelcomeAction } from "../src/welcome.ts";
|
|
43
46
|
|
|
44
47
|
const WELCOME_KEY = "pi-jar.welcome";
|
|
@@ -48,6 +51,9 @@ const VERSION = (() => {
|
|
|
48
51
|
})();
|
|
49
52
|
/** Ignore the click Pi may synthesize right after a press we already acted on. */
|
|
50
53
|
const WELCOME_CLICK_DEDUPE_MS = 400;
|
|
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"]);
|
|
51
57
|
export default function piJar(pi: ExtensionAPI): void {
|
|
52
58
|
installCompactBuiltinTools(pi);
|
|
53
59
|
let demo = false;
|
|
@@ -74,9 +80,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
74
80
|
let footerSettings = visualSettings.footer;
|
|
75
81
|
let welcomeStatuses = (): ReadonlyMap<string, string> => new Map();
|
|
76
82
|
const working = new WorkingState();
|
|
77
|
-
let workingClock: ReturnType<typeof setInterval> | undefined;
|
|
78
83
|
let workingIndicatorKey = "";
|
|
79
|
-
const stopWorkingClock = () => { if (workingClock) clearInterval(workingClock); workingClock = undefined; };
|
|
80
84
|
let openSettings: (ctx: ExtensionContext) => Promise<void> = async () => {};
|
|
81
85
|
let settingsOpen = false;
|
|
82
86
|
|
|
@@ -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, {
|
|
@@ -158,8 +221,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
158
221
|
const suggestions = new SuggestionState();
|
|
159
222
|
composer.attachSuggestions(suggestions);
|
|
160
223
|
const suggest = registerSuggestions(pi, suggestions, {
|
|
161
|
-
enabled: () => enabled && visualSettings.composer && visualSettings.suggestions && composer.enabled
|
|
162
|
-
skip: () => planMode.isEnabled() || !!goals?.isActive()
|
|
224
|
+
enabled: () => enabled && visualSettings.composer && visualSettings.suggestions && composer.enabled
|
|
163
225
|
});
|
|
164
226
|
let liveTui: { setCopyOnSelect?: (enabled: boolean) => void } | undefined;
|
|
165
227
|
/** Pi's own settings (TUI mode, copy-on-select); written through Pi's SettingsManager. */
|
|
@@ -175,8 +237,6 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
175
237
|
};
|
|
176
238
|
|
|
177
239
|
const applyWorking = (ctx: ExtensionContext) => {
|
|
178
|
-
const active = enabled && working.phase !== "idle";
|
|
179
|
-
if (!active || !ctx.hasUI || ctx.mode !== "tui") stopWorkingClock();
|
|
180
240
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
181
241
|
composer.setActivity(enabled ? working.phase : "idle", animations);
|
|
182
242
|
try {
|
|
@@ -187,14 +247,12 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
187
247
|
ctx.ui.setWorkingMessage?.(view.message);
|
|
188
248
|
const indicatorKey = `${working.phase}:${animations}`;
|
|
189
249
|
if (indicatorKey !== workingIndicatorKey) {
|
|
190
|
-
|
|
250
|
+
// Pi's normal streaming/tool lifecycle already causes renders. A static indicator avoids
|
|
251
|
+
// forcing the entire transcript to repaint on a timer in long sessions.
|
|
252
|
+
ctx.ui.setWorkingIndicator({ frames: [view.frames[0] ?? "✢"] });
|
|
191
253
|
workingIndicatorKey = indicatorKey;
|
|
192
254
|
}
|
|
193
|
-
|
|
194
|
-
workingClock = setInterval(() => applyWorking(ctx), 1000);
|
|
195
|
-
workingClock.unref?.();
|
|
196
|
-
}
|
|
197
|
-
} catch { stopWorkingClock(); /* Working decoration must never interrupt a Pi turn. */ }
|
|
255
|
+
} catch { /* Working decoration must never interrupt a Pi turn. */ }
|
|
198
256
|
};
|
|
199
257
|
|
|
200
258
|
const stopWelcome = (ctx?: ExtensionContext) => {
|
|
@@ -358,32 +416,22 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
358
416
|
welcomeStatuses = () => footerData.getExtensionStatuses();
|
|
359
417
|
welcomeBranch = () => footerData.getGitBranch();
|
|
360
418
|
let frame = 0;
|
|
361
|
-
let memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
362
|
-
let memoryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
363
|
-
const MEMORY_SAMPLE_MS = 10_000;
|
|
364
|
-
const sampleMemory = () => {
|
|
365
|
-
if (disposed) return;
|
|
366
|
-
const next = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
367
|
-
if (next !== memory) { memory = next; tui.requestRender(); }
|
|
368
|
-
memoryTimer = setTimeout(sampleMemory, MEMORY_SAMPLE_MS);
|
|
369
|
-
memoryTimer.unref?.();
|
|
370
|
-
};
|
|
371
|
-
if (footerSettings.memory) {
|
|
372
|
-
memoryTimer = setTimeout(sampleMemory, MEMORY_SAMPLE_MS);
|
|
373
|
-
memoryTimer.unref?.();
|
|
374
|
-
}
|
|
375
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
376
419
|
let expiryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
420
|
+
let expiryAt: number | undefined;
|
|
421
|
+
let memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
422
|
+
let memorySampledAt = 0;
|
|
423
|
+
let context = "ctx ?";
|
|
424
|
+
let contextSampledAt = 0;
|
|
377
425
|
let disposed = false;
|
|
426
|
+
let hits: FooterHit[] = [];
|
|
427
|
+
let pressed: { target: FooterTarget; at: number } | undefined;
|
|
378
428
|
const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
|
|
379
429
|
const dispose = () => {
|
|
380
430
|
if (disposed) return;
|
|
381
431
|
disposed = true;
|
|
382
|
-
if (memoryTimer) clearTimeout(memoryTimer);
|
|
383
|
-
if (timer) clearInterval(timer);
|
|
384
432
|
if (expiryTimer) clearTimeout(expiryTimer);
|
|
385
|
-
timer = undefined;
|
|
386
433
|
expiryTimer = undefined;
|
|
434
|
+
expiryAt = undefined;
|
|
387
435
|
if (footerTui === tui) { footerTui = undefined; welcomeStatuses = () => new Map<string, string>(); welcomeBranch = () => null; }
|
|
388
436
|
unsubscribe();
|
|
389
437
|
};
|
|
@@ -391,38 +439,67 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
391
439
|
return {
|
|
392
440
|
dispose,
|
|
393
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
|
+
},
|
|
394
455
|
render(width: number): string[] {
|
|
395
456
|
try {
|
|
396
457
|
const now = Date.now();
|
|
397
458
|
const statuses = footerData.getExtensionStatuses();
|
|
398
459
|
const live = collectStatuses(statuses, now);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
expiryTimer = undefined;
|
|
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-"));
|
|
402
462
|
const nearest = demo ? undefined : roles.reduce<number | undefined>((min, role) =>
|
|
403
463
|
role.expiresAt != null ? Math.min(min ?? Infinity, role.expiresAt) : min, undefined);
|
|
404
|
-
if (nearest
|
|
464
|
+
if (nearest !== expiryAt) {
|
|
465
|
+
if (expiryTimer) clearTimeout(expiryTimer);
|
|
466
|
+
expiryTimer = undefined;
|
|
467
|
+
expiryAt = nearest;
|
|
468
|
+
if (nearest != null) {
|
|
469
|
+
expiryTimer = setTimeout(() => { expiryTimer = undefined; expiryAt = undefined; tui.requestRender(); }, Math.max(1, nearest - now));
|
|
470
|
+
expiryTimer.unref?.();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Do not schedule cosmetic footer repaints. Role state still updates on Pi's native
|
|
474
|
+
// stream/tool renders, which keeps long transcripts responsive.
|
|
405
475
|
const active = animations && footerSettings.roles && roles.some((role) => ACTIVE_STATES.has(role.state));
|
|
406
|
-
if (active
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
476
|
+
if (active) frame++;
|
|
477
|
+
if (now - contextSampledAt >= 1000) {
|
|
478
|
+
const usage = ctx.getContextUsage();
|
|
479
|
+
context = usage?.percent == null || !Number.isFinite(usage.percent)
|
|
480
|
+
? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
|
|
481
|
+
contextSampledAt = now;
|
|
482
|
+
}
|
|
483
|
+
if (now - memorySampledAt >= 5000) {
|
|
484
|
+
memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
|
|
485
|
+
memorySampledAt = now;
|
|
486
|
+
}
|
|
413
487
|
const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, now) : undefined;
|
|
414
|
-
|
|
488
|
+
const layout = renderFooterLayout({
|
|
415
489
|
model: ctx.model?.id ?? "no-model", effort: ctx.model?.reasoning === false ? "off" : (pi.getThinkingLevel?.() ?? "off"),
|
|
416
490
|
sessionName: ctx.sessionManager?.getSessionName?.(),
|
|
417
491
|
cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
|
|
418
|
-
context, goal: goalLoop.progress(), chips: footerChips(),
|
|
492
|
+
context, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
|
|
493
|
+
memory, cost: formatCost(cost), quota, roles, extras: live.extras,
|
|
419
494
|
demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
|
|
420
495
|
}, width, ctx.ui.theme ?? theme);
|
|
496
|
+
hits = layout.hits;
|
|
497
|
+
return layout.lines;
|
|
421
498
|
} catch {
|
|
422
|
-
|
|
499
|
+
hits = [];
|
|
423
500
|
if (expiryTimer) clearTimeout(expiryTimer);
|
|
424
|
-
timer = undefined;
|
|
425
501
|
expiryTimer = undefined;
|
|
502
|
+
expiryAt = undefined;
|
|
426
503
|
return width > 0 ? ["pi-jar: UI unavailable (run /jar ui off)".slice(0, width)] : [];
|
|
427
504
|
}
|
|
428
505
|
}
|
|
@@ -443,6 +520,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
443
520
|
return;
|
|
444
521
|
}
|
|
445
522
|
visualSettings = next;
|
|
523
|
+
setIconSet(next.icons);
|
|
446
524
|
enabled = next.ui;
|
|
447
525
|
animations = next.animations;
|
|
448
526
|
if (footerSettings.quota && !next.footer.quota) quotaCache?.stop();
|
|
@@ -481,24 +559,28 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
481
559
|
try { cost = sessionCost(ctx); } catch { cost = 0; }
|
|
482
560
|
footerTui?.requestRender();
|
|
483
561
|
};
|
|
484
|
-
const
|
|
485
|
-
|
|
562
|
+
const addMessageCost = (message: { usage?: { cost?: { total?: number } } }) => {
|
|
563
|
+
const value = message.usage?.cost?.total;
|
|
564
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) cost += value;
|
|
565
|
+
};
|
|
566
|
+
const restoreTodos = (ctx: ExtensionContext, branch?: readonly unknown[]) => {
|
|
567
|
+
try { todos?.restore(branch ?? sessionBranch(ctx)); } catch { todos?.restore([]); }
|
|
486
568
|
updateTaskWidget(ctx);
|
|
487
569
|
};
|
|
488
|
-
const restoreGoal = (ctx: ExtensionContext) => {
|
|
489
|
-
try { goals?.restore(ctx
|
|
570
|
+
const restoreGoal = (ctx: ExtensionContext, branch?: readonly unknown[]) => {
|
|
571
|
+
try { goals?.restore(branch ?? sessionBranch(ctx)); } catch { goals?.restore([]); }
|
|
490
572
|
footerTui?.requestRender();
|
|
491
573
|
};
|
|
492
574
|
pi.on("session_start", (_event, ctx) => {
|
|
493
575
|
sideUsage.clear();
|
|
494
576
|
migrateLegacySettings(getAgentDir());
|
|
495
577
|
visualSettings = loadVisualSettings(getAgentDir());
|
|
578
|
+
setIconSet(visualSettings.icons);
|
|
496
579
|
animations = visualSettings.animations;
|
|
497
580
|
enabled = visualSettings.ui;
|
|
498
581
|
footerSettings = visualSettings.footer;
|
|
499
582
|
demo = false;
|
|
500
583
|
composer.disable(ctx);
|
|
501
|
-
stopWorkingClock();
|
|
502
584
|
workingIndicatorKey = "";
|
|
503
585
|
working.end();
|
|
504
586
|
quotaCache?.stop();
|
|
@@ -506,11 +588,14 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
506
588
|
changes = new ChangeTracker(() => ctx.cwd);
|
|
507
589
|
changeCount = 0;
|
|
508
590
|
shells?.dispose();
|
|
591
|
+
subagents.clear();
|
|
509
592
|
shells = new ShellManager(onShellEvent);
|
|
510
593
|
shells.onChange = () => footerTui?.requestRender();
|
|
511
594
|
goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
|
|
512
|
-
|
|
513
|
-
|
|
595
|
+
let branch: readonly unknown[] = [];
|
|
596
|
+
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
597
|
+
restoreTodos(ctx, branch);
|
|
598
|
+
restoreGoal(ctx, branch);
|
|
514
599
|
// Quota is enabled per session; no credentials or consent are persisted.
|
|
515
600
|
quotaCache = new QuotaCache(
|
|
516
601
|
(provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
|
|
@@ -528,7 +613,20 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
528
613
|
if (visualSettings.composer && enabled) composer.enable(ctx);
|
|
529
614
|
suggest.sync();
|
|
530
615
|
if (visualSettings.accent !== "follow") selectAccent(ctx, visualSettings.accent);
|
|
531
|
-
|
|
616
|
+
if (branch.length >= LARGE_SESSION_ENTRIES) {
|
|
617
|
+
try {
|
|
618
|
+
const prefs = piPreferences(ctx)?.get();
|
|
619
|
+
if (prefs && !prefs.fullscreen) {
|
|
620
|
+
ctx.ui.notify(`Large session (${branch.length} entries): regular TUI redraw cost grows with history. Enable Fullscreen in /jar settings for smoother rendering.`, "warning");
|
|
621
|
+
}
|
|
622
|
+
} catch { /* Performance hint is optional. */ }
|
|
623
|
+
}
|
|
624
|
+
// Resumed sessions already have a transcript to paint. Rebuilding the animated welcome and
|
|
625
|
+
// scanning other session files competes with that expensive initial render for no real benefit.
|
|
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);
|
|
532
630
|
});
|
|
533
631
|
// Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
|
|
534
632
|
const dismissWelcome = (ctx: ExtensionContext) => {
|
|
@@ -553,6 +651,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
553
651
|
pi.on("message_end", (event, ctx) => {
|
|
554
652
|
if (event.message.role === "assistant") {
|
|
555
653
|
working.reportOutputTokens(event.message.usage?.output ?? 0);
|
|
654
|
+
addMessageCost(event.message);
|
|
556
655
|
applyWorking(ctx);
|
|
557
656
|
}
|
|
558
657
|
});
|
|
@@ -561,19 +660,26 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
561
660
|
pi.on("ui_prompt_start", (_event, ctx) => { working.prompt(true); applyWorking(ctx); });
|
|
562
661
|
pi.on("ui_prompt_end", (_event, ctx) => { working.prompt(false); applyWorking(ctx); });
|
|
563
662
|
// A request may span several tool turns; keep its elapsed time and reported tokens until agent_end.
|
|
564
|
-
pi.on("turn_end", (_event, ctx) => { applyWorking(ctx);
|
|
663
|
+
pi.on("turn_end", (_event, ctx) => { applyWorking(ctx); });
|
|
565
664
|
pi.on("agent_end", (_event, ctx) => { working.end(); applyWorking(ctx); });
|
|
566
665
|
pi.on("agent_before_settle", (event) => { if (event.outcome === "error") composer.flash("error"); });
|
|
567
666
|
pi.on("agent_settled", (_event, ctx) => { working.end(); applyWorking(ctx); });
|
|
568
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
667
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
668
|
+
let branch: readonly unknown[] = [];
|
|
669
|
+
try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
|
|
670
|
+
restoreTodos(ctx, branch);
|
|
671
|
+
restoreGoal(ctx, branch);
|
|
672
|
+
updateCost(ctx);
|
|
673
|
+
composer.refreshSession(ctx);
|
|
674
|
+
});
|
|
569
675
|
pi.on("session_compact", (_event, ctx) => { restoreTodos(ctx); restoreGoal(ctx); updateCost(ctx); });
|
|
570
676
|
pi.on("model_select", (_event, _ctx) => footerTui?.requestRender());
|
|
571
677
|
pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
|
|
572
678
|
pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); footerTui?.requestRender(); });
|
|
573
679
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
680
|
+
clearSessionBranchCache(ctx);
|
|
574
681
|
stopWelcome(ctx);
|
|
575
682
|
composer.disable(ctx);
|
|
576
|
-
stopWorkingClock();
|
|
577
683
|
workingIndicatorKey = "";
|
|
578
684
|
working.end();
|
|
579
685
|
try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
|
|
@@ -584,6 +690,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
584
690
|
changeCount = 0;
|
|
585
691
|
shells?.dispose();
|
|
586
692
|
shells = undefined;
|
|
693
|
+
subagents.clear();
|
|
587
694
|
goals = undefined;
|
|
588
695
|
disposeFooter?.();
|
|
589
696
|
disposeFooter = undefined;
|
|
@@ -607,7 +714,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
607
714
|
earlier = (await SessionManager.list(ctx.cwd)).filter((session) => session.path !== current)
|
|
608
715
|
.sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 50);
|
|
609
716
|
} catch (error) { ctx.ui.notify("pi-jar: earlier sessions unavailable: " + String(error), "warning"); }
|
|
610
|
-
const picked = await openPromptSearch(ctx, collectPrompts(ctx
|
|
717
|
+
const picked = await openPromptSearch(ctx, collectPrompts(sessionBranch(ctx), earlier));
|
|
611
718
|
if (picked !== undefined) ctx.ui.setEditorText(picked);
|
|
612
719
|
}
|
|
613
720
|
});
|
|
@@ -615,6 +722,10 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
615
722
|
description: "Cycle pi-jar model roles",
|
|
616
723
|
handler: async (ctx) => { await modelRoles.cycle(ctx); footerTui?.requestRender(); }
|
|
617
724
|
});
|
|
725
|
+
pi.registerShortcut?.(Key.ctrlAlt("a"), {
|
|
726
|
+
description: "Subagents and background shells (pi-jar)",
|
|
727
|
+
handler: async (ctx) => { await openActivity(ctx); }
|
|
728
|
+
});
|
|
618
729
|
pi.registerShortcut?.(Key.ctrlAlt("s"), {
|
|
619
730
|
description: "Open pi-jar settings",
|
|
620
731
|
handler: async (ctx) => {
|
|
@@ -638,25 +749,24 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
638
749
|
}
|
|
639
750
|
// `/jar resume` without a number opens the same searchable picker.
|
|
640
751
|
if (command === "sessions" || command.startsWith("sessions ") || command === "resume") {
|
|
641
|
-
|
|
642
|
-
let sessions;
|
|
643
|
-
try { sessions = (await SessionManager.list(ctx.cwd)).sort((a, b) => b.modified.getTime() - a.modified.getTime()); }
|
|
644
|
-
catch (error) { ctx.ui.notify("Could not search sessions: " + String(error), "error"); return; }
|
|
645
|
-
if (!sessions.length) { ctx.ui.notify("No sessions found for this project", "info"); return; }
|
|
646
|
-
const selected = await pickSession(ctx, sessions, command === "resume" ? "" : args.trim().slice(8).trim());
|
|
647
|
-
if (selected && selected !== ctx.sessionManager.getSessionFile()) {
|
|
648
|
-
// Never access the old command context after a successful switch.
|
|
649
|
-
const result = await ctx.switchSession(selected);
|
|
650
|
-
if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
|
|
651
|
-
}
|
|
752
|
+
await openSessions(ctx, command === "resume" ? "" : args.trim().slice(8).trim());
|
|
652
753
|
return;
|
|
653
754
|
}
|
|
654
755
|
if (command.startsWith("resume ")) {
|
|
655
|
-
const
|
|
756
|
+
const arg = args.trim().slice(7).trim();
|
|
757
|
+
const index = Number(arg);
|
|
656
758
|
let recent = welcomeRecent;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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; }
|
|
660
770
|
const result = await ctx.switchSession(target.path);
|
|
661
771
|
if (result.cancelled) ctx.ui.notify("Session switch cancelled", "info");
|
|
662
772
|
return;
|
|
@@ -751,10 +861,23 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
751
861
|
}
|
|
752
862
|
if (command === "welcome") { showWelcome(ctx); return; }
|
|
753
863
|
if (command === "commit" || command.startsWith("commit ")) { await jarCommit(pi, ctx, modelRoles, sideUsage, args.trim().slice(6).trim()); return; }
|
|
754
|
-
if (command === "shells") {
|
|
755
|
-
if (!
|
|
756
|
-
|
|
757
|
-
|
|
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");
|
|
758
881
|
return;
|
|
759
882
|
}
|
|
760
883
|
if (command === "demo") demo = true;
|
|
@@ -766,7 +889,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
766
889
|
else if (command === "quota on" && quotaCache) { quotaCache.enabled = true; quotaDisabledByUser = false; }
|
|
767
890
|
else if (command === "quota off" && quotaCache) { quotaCache.enabled = false; quotaDisabledByUser = true; quotaCache.stop(); }
|
|
768
891
|
else {
|
|
769
|
-
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");
|
|
770
893
|
return;
|
|
771
894
|
}
|
|
772
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",
|