pi-jar 0.1.9 → 0.2.1
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 -5
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/WORKFLOWS.md +1 -1
- package/extensions/index.ts +167 -14
- package/package.json +2 -2
- package/src/advisor.ts +226 -73
- package/src/composer.ts +21 -4
- package/src/context-diet.ts +94 -0
- package/src/delegate-worktree.ts +33 -13
- package/src/delegate.ts +70 -40
- package/src/model-roles.ts +29 -9
- package/src/profile-ui.ts +58 -0
- package/src/profiles.ts +148 -0
- package/src/settings-ui.ts +36 -14
- package/src/settings.ts +4 -1
- package/src/shell-notifications.ts +33 -6
- package/src/shells.ts +4 -1
- package/src/side-model.ts +3 -1
- package/src/welcome.ts +6 -1
package/src/settings.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface JarVisualSettings {
|
|
|
23
23
|
advisor: boolean;
|
|
24
24
|
/** Consult the advisor automatically when the agent repeats a tool call or keeps failing. */
|
|
25
25
|
advisorGates: boolean;
|
|
26
|
+
/** Opt-in provider-context pruning of reasoning from completed earlier user turns. */
|
|
27
|
+
contextDiet: boolean;
|
|
26
28
|
/** Glyph set for footer, composer and views: unicode, Nerd Font icons, or plain ascii. */
|
|
27
29
|
icons: IconSet;
|
|
28
30
|
footer: FooterSettings;
|
|
@@ -37,7 +39,7 @@ export function isMaxSubagents(value: unknown): value is MaxSubagents {
|
|
|
37
39
|
|
|
38
40
|
export const SETTINGS_FILE = "pi-jar-settings.json";
|
|
39
41
|
export function defaultVisualSettings(footer: FooterSettings = DEFAULT_FOOTER_SETTINGS): JarVisualSettings {
|
|
40
|
-
return { version: 1, accent: "follow", animations: true, ui: true, composer: true, mascot: true, suggestions: true, goalRounds: 8, maxSubagents: 4, advisor: true, advisorGates: true, icons: "unicode", footer: { ...footer } };
|
|
42
|
+
return { version: 1, accent: "follow", animations: true, ui: true, composer: true, mascot: true, suggestions: true, goalRounds: 8, maxSubagents: 4, advisor: true, advisorGates: true, contextDiet: false, icons: "unicode", footer: { ...footer } };
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** Legacy footer choices are imported only while the new file is absent. Never modify the old file. */
|
|
@@ -70,6 +72,7 @@ export function loadVisualSettings(directory: string): JarVisualSettings {
|
|
|
70
72
|
maxSubagents: isMaxSubagents(value.maxSubagents) ? value.maxSubagents : fallback.maxSubagents,
|
|
71
73
|
advisor: typeof value.advisor === "boolean" ? value.advisor : fallback.advisor,
|
|
72
74
|
advisorGates: typeof value.advisorGates === "boolean" ? value.advisorGates : fallback.advisorGates,
|
|
75
|
+
contextDiet: typeof value.contextDiet === "boolean" ? value.contextDiet : fallback.contextDiet,
|
|
73
76
|
icons: ICON_SETS.some((set) => set === value.icons) ? value.icons as IconSet : fallback.icons,
|
|
74
77
|
footer: Object.fromEntries(FOOTER_FIELDS.map((field) => [field,
|
|
75
78
|
typeof footer[field] === "boolean" ? footer[field] : fallback.footer[field]])) as FooterSettings
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, CustomMessageEntryDraft } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, CustomMessageEntryDraft } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { cleanText } from "./status.ts";
|
|
4
4
|
import { describe, SHELL_MESSAGE, type ShellEvent, type ShellManager } from "./shells.ts";
|
|
@@ -34,14 +34,40 @@ export function registerShellNotifications(pi: ExtensionAPI, manager: () => Shel
|
|
|
34
34
|
let interrupted = false;
|
|
35
35
|
let deliveryFailures = 0;
|
|
36
36
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
37
|
+
let feedbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
let context: ExtensionContext | undefined;
|
|
39
|
+
const shown = new Set<string>();
|
|
40
|
+
const eventKey = (event: ShellEvent) => `${event.kind}:${event.job.id}`;
|
|
37
41
|
const warned = new Set<string>();
|
|
38
|
-
const cancelTimer = () => { clearTimeout(timer); timer = undefined; };
|
|
42
|
+
const cancelTimer = () => { clearTimeout(timer); timer = undefined; clearTimeout(feedbackTimer); feedbackTimer = undefined; };
|
|
39
43
|
const take = () => {
|
|
40
44
|
cancelTimer();
|
|
41
45
|
const events = manager()?.takeNotifications() ?? [];
|
|
46
|
+
shown.clear();
|
|
42
47
|
return events.length ? shellNotification(events) : undefined;
|
|
43
48
|
};
|
|
44
49
|
const notify = () => {
|
|
50
|
+
// UI feedback must not wait for the remote model to finish reasoning. Keep model transport
|
|
51
|
+
// boundary-safe and acknowledgeable: this preview does not consume or enqueue a follow-up.
|
|
52
|
+
if (active && !interrupted && context?.hasUI && !feedbackTimer && manager()?.pendingNotifications()) {
|
|
53
|
+
const owner = manager(), ctx = context;
|
|
54
|
+
feedbackTimer = setTimeout(() => {
|
|
55
|
+
feedbackTimer = undefined;
|
|
56
|
+
if (!active || interrupted || owner !== manager() || ctx !== context) return;
|
|
57
|
+
const snapshot = owner?.notificationSnapshot() ?? [];
|
|
58
|
+
const known = new Set(snapshot.map(eventKey));
|
|
59
|
+
for (const key of shown) if (!known.has(key)) shown.delete(key);
|
|
60
|
+
const events = snapshot.filter(event => !shown.has(eventKey(event)));
|
|
61
|
+
if (!events.length) return;
|
|
62
|
+
const summary = shellNotification(events).details as ShellNotificationDetails;
|
|
63
|
+
const text = `${summary.summary}: ${events.slice(0, 3).map(event => describe(event.job)).join("; ")}. Model delivery waits for a safe turn boundary.`;
|
|
64
|
+
try {
|
|
65
|
+
ctx.ui.notify(cleanText(text, 420), events.some(event => event.job.exitCode !== 0 && event.kind === "exit") ? "warning" : "info");
|
|
66
|
+
for (const event of events) shown.add(eventKey(event));
|
|
67
|
+
} catch (error) { console.error("pi-jar: could not show shell completion", error); }
|
|
68
|
+
}, 300);
|
|
69
|
+
feedbackTimer.unref?.();
|
|
70
|
+
}
|
|
45
71
|
if (active || interrupted || timer || !manager()?.pendingNotifications()) return;
|
|
46
72
|
const owner = manager();
|
|
47
73
|
timer = setTimeout(() => {
|
|
@@ -59,16 +85,17 @@ export function registerShellNotifications(pi: ExtensionAPI, manager: () => Shel
|
|
|
59
85
|
}, 300);
|
|
60
86
|
timer.unref?.();
|
|
61
87
|
};
|
|
62
|
-
const reset = () => { cancelTimer(); active = false; interrupted = false; deliveryFailures = 0; warned.clear(); };
|
|
88
|
+
const reset = () => { cancelTimer(); active = false; interrupted = false; context = undefined; deliveryFailures = 0; warned.clear(); shown.clear(); };
|
|
63
89
|
|
|
64
90
|
pi.registerMessageRenderer?.<ShellNotificationDetails>(SHELL_MESSAGE, (message, { expanded }, theme) => {
|
|
65
91
|
const text = typeof message.content === "string" ? message.content : "Background shells";
|
|
66
92
|
const summary = message.details?.summary ?? text.split("\n", 1)[0] ?? "Background shells";
|
|
67
93
|
return new Text(expanded ? text : theme.fg("accent", cleanText(summary, 160)) + theme.fg("dim", " · expand for status"), 0, 0);
|
|
68
94
|
});
|
|
69
|
-
pi.on?.("session_start", reset);
|
|
95
|
+
pi.on?.("session_start", (_event, ctx) => { reset(); context = ctx; });
|
|
70
96
|
pi.on?.("session_shutdown", reset);
|
|
71
|
-
pi.on?.("before_agent_start", () => {
|
|
97
|
+
pi.on?.("before_agent_start", (_event, ctx) => {
|
|
98
|
+
context = ctx;
|
|
72
99
|
active = true;
|
|
73
100
|
interrupted = false;
|
|
74
101
|
const entry = take();
|
|
@@ -76,7 +103,7 @@ export function registerShellNotifications(pi: ExtensionAPI, manager: () => Shel
|
|
|
76
103
|
const { type: _type, ...message } = entry;
|
|
77
104
|
return { message };
|
|
78
105
|
});
|
|
79
|
-
pi.on?.("agent_start", () => { active = true; cancelTimer(); });
|
|
106
|
+
pi.on?.("agent_start", (_event, ctx) => { context = ctx; active = true; cancelTimer(); });
|
|
80
107
|
pi.on?.("turn_end", event => {
|
|
81
108
|
if (event.outcome !== "completed" || event.context?.canContinue === false) return;
|
|
82
109
|
const entry = take();
|
package/src/shells.ts
CHANGED
|
@@ -125,6 +125,8 @@ export class ShellManager {
|
|
|
125
125
|
for (const event of events) if (this.jobs.has(event.job.id) && !this.pending.has(event.job.id)) this.pending.set(event.job.id, event);
|
|
126
126
|
}
|
|
127
127
|
pendingNotifications(): number { return this.pending.size; }
|
|
128
|
+
/** Non-consuming status snapshots for immediate UI feedback; never copy or expose log lines. */
|
|
129
|
+
notificationSnapshot(): ShellEvent[] { return [...this.pending.values()].map(event => ({ ...event, job: { ...event.job, lines: [] } })); }
|
|
128
130
|
verificationPending(): Array<Omit<ShellJob, "lines">> {
|
|
129
131
|
return [...this.jobs.values()].filter(entry => !entry.finished && entry.job.purpose !== "service").map(({ job }) => {
|
|
130
132
|
const { lines: _lines, ...summary } = job;
|
|
@@ -438,6 +440,7 @@ export function registerShells(pi: ExtensionAPI, shells: () => ShellManager | un
|
|
|
438
440
|
promptGuidelines: [
|
|
439
441
|
"Use jar_shell start for servers, watchers and slow commands; set watch to a regex for the line you are waiting for (e.g. \"listening|ready|error\").",
|
|
440
442
|
"Do not poll: work while commands run, then use wait with explicit ids for relevant checks before the final summary. A bounded wait returns pending jobs honestly; never claim success without their exit codes. Do not wait for services/watchers. output, peek, list and wait acknowledge observed events, preventing stale replay.",
|
|
443
|
+
"Completion events cannot interrupt an in-flight model response. If no useful work remains, call wait for the relevant finite jobs immediately instead of thinking or waiting for an automatic wake-up.",
|
|
441
444
|
"Notifications contain coalesced status, not log dumps. Inspect output only for relevant diagnostics. Mark never-ending jobs purpose: service and kill shells you no longer need."
|
|
442
445
|
],
|
|
443
446
|
parameters: Parameters,
|
|
@@ -453,7 +456,7 @@ export function registerShells(pi: ExtensionAPI, shells: () => ShellManager | un
|
|
|
453
456
|
case "start": {
|
|
454
457
|
const job = manager.start({ command: params.command ?? "", cwd: ctx.cwd, ...(params.name ? { name: params.name } : {}), ...(params.watch ? { watch: params.watch } : {}),
|
|
455
458
|
...(params.notify !== undefined ? { notify: params.notify } : {}), ...(params.purpose ? { purpose: params.purpose } : {}) });
|
|
456
|
-
return reply(`Started ${describe(job)} (pid ${job.pid ?? "?"}). ${job.notify ? "
|
|
459
|
+
return reply(`Started ${describe(job)} (pid ${job.pid ?? "?"}). ${job.notify ? "Completion reaches the model at the next safe turn boundary (or wakes it when idle). If no other useful work remains, use wait now." : "Notifications are off; check it with output."}`);
|
|
457
460
|
}
|
|
458
461
|
case "output": {
|
|
459
462
|
if (!params.id) return reply("output needs an id.");
|
package/src/side-model.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface SideAnswer { text: string; model: string }
|
|
|
30
30
|
* Ask the model assigned to `role` (or the current model) a one-shot question outside the
|
|
31
31
|
* conversation. Tries configured fallbacks in order on failure; cancellation never retries.
|
|
32
32
|
*/
|
|
33
|
-
export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, usage: SideUsage | undefined, role: string,
|
|
33
|
+
export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, usage: Pick<SideUsage, "add"> | undefined, role: string,
|
|
34
34
|
systemPrompt: string, prompt: string, signal?: AbortSignal): Promise<SideAnswer> {
|
|
35
35
|
const fallbacks = roles.fallbackSpecs?.(role) ?? [];
|
|
36
36
|
const failures: string[] = [];
|
|
@@ -52,6 +52,8 @@ export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, us
|
|
|
52
52
|
const result = await ctx.modelRegistry.streamSimple(model,
|
|
53
53
|
{ systemPrompt, messages: [{ role: "user", content: prompt, timestamp: Date.now() }] },
|
|
54
54
|
{ ...(reasoning ? { reasoning } : {}), ...(signal ? { signal } : {}) }).result();
|
|
55
|
+
// Callers with detached work can supply a session-generation-safe usage sink.
|
|
56
|
+
// Returned attempts (including cancelled attempts) still account for actual usage.
|
|
55
57
|
const cost = result.usage?.cost?.total;
|
|
56
58
|
usage?.add({ role, model: name, input: result.usage?.input ?? 0, output: result.usage?.output ?? 0, cacheRead: result.usage?.cacheRead ?? 0,
|
|
57
59
|
cacheWrite: result.usage?.cacheWrite ?? 0, cost: Number.isFinite(cost) ? cost! : 0, at: Date.now() });
|
package/src/welcome.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
1
|
+
import { sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { FLAME_RAMP, FLAME_WIDTH, flameFrame, rgb, supportsTruecolor } from "./flame.ts";
|
|
3
3
|
import { cleanText } from "./status.ts";
|
|
4
4
|
import { iconSet, withIcon } from "./icons.ts";
|
|
@@ -10,6 +10,8 @@ export interface WelcomeInfo {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
effort?: string;
|
|
12
12
|
activeRole?: string;
|
|
13
|
+
/** Active configuration profile, shown even before a session is named. */
|
|
14
|
+
profile?: string;
|
|
13
15
|
project?: string;
|
|
14
16
|
context?: string;
|
|
15
17
|
cost?: string;
|
|
@@ -189,6 +191,8 @@ function card(width: number, fg: Paint, info: WelcomeInfo): string[] {
|
|
|
189
191
|
cell(center(fg("dim", "— welcome to ") + fg("accent", "pi-jar") + fg("dim", " —"), inner)),
|
|
190
192
|
cell(""),
|
|
191
193
|
divider,
|
|
194
|
+
...wrapTextWithAnsi(cleanText(info.profile ?? "Default", 32) || "Default", Math.max(1, inner - labelWidth))
|
|
195
|
+
.map((line, index) => cell(index ? fg("dim", " ".repeat(labelWidth)) + fg("accent", line) : label("PROFILE", line, "accent"))),
|
|
192
196
|
cell(label("PROJECT", withIcon("folder", info.project || "unavailable") + " · " + git, info.dirty ? "warning" : "muted")),
|
|
193
197
|
cell(label("SESSION", [info.context ?? "ctx ?", quota, info.cost ?? ""].filter(Boolean).join(" · "))),
|
|
194
198
|
divider,
|
|
@@ -262,6 +266,7 @@ function renderWelcome(width: number, frame: number, fg: Paint, info: WelcomeInf
|
|
|
262
266
|
...flame.slice(4).map((line) => fit(sliceByColumn(line, start, Math.min(width, FLAME_WIDTH)))),
|
|
263
267
|
...PI_COMPACT.map((line) => fit(fg("accent", line))),
|
|
264
268
|
fit(fg("accent", "pi-jar")),
|
|
269
|
+
fit(fg("accent", "Profile: " + (cleanText(info.profile ?? "Default", 32) || "Default"))),
|
|
265
270
|
...wrap(cleanText(info.message ?? HOPEFUL_WELCOME_MESSAGES[0], 100), width).map((line) => fit("\x1b[1m" + fg("warning", line) + "\x1b[22m")),
|
|
266
271
|
fit(fg(info.settingsClickable === false ? "dim" : "accent", info.settingsClickable === false ? "ctrl+alt+s settings" : "[ ⚙ Settings ]")),
|
|
267
272
|
""
|