pi-jar 0.2.8 → 0.3.0
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 +20 -21
- package/docs/DIFF-REVIEW.md +5 -4
- package/docs/EFFICIENCY_PLAN.md +8 -9
- package/docs/FLAME.md +19 -16
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/SHOWCASE.md +43 -1
- package/docs/SUBAGENT-TOOLS.md +6 -0
- package/extensions/index.ts +6 -60
- package/package.json +3 -3
- package/src/cache-breaks.ts +12 -1
- package/src/compact-tools.ts +23 -1
- package/src/composer.ts +55 -39
- package/src/delegate.ts +21 -7
- package/src/diff-inline.ts +14 -6
- package/src/diff-view.ts +32 -9
- package/src/footer.ts +0 -4
- package/src/mascot.ts +134 -22
- package/src/profiles.ts +5 -6
- package/src/settings-ui.ts +13 -30
- package/src/settings.ts +1 -21
- package/src/subagent-tools.ts +28 -4
- package/src/task-tool.ts +2 -1
- package/themes/pi-jar-dark-amber.json +2 -1
- package/themes/pi-jar-dark-azure.json +2 -1
- package/themes/pi-jar-dark-gray.json +2 -1
- package/themes/pi-jar-dark-pink.json +2 -1
- package/themes/pi-jar-dark-teal.json +2 -1
- package/themes/pi-jar-dark-violet.json +2 -1
- package/themes/pi-jar-dark.json +2 -1
- package/src/context-budget.ts +0 -226
- package/src/context-diet.ts +0 -235
package/src/mascot.ts
CHANGED
|
@@ -2,35 +2,142 @@ import { FLAME_RAMP, rgb, supportsTruecolor } from "./flame.ts";
|
|
|
2
2
|
import type { WorkingPhase } from "./working.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Ember
|
|
6
|
-
*
|
|
5
|
+
* Ember's activity state drives a shaded 11×10 pixel pet above the composer.
|
|
6
|
+
* Short/narrow terminals retain a five-column icon and crown; both layouts have fixed dimensions.
|
|
7
7
|
*/
|
|
8
8
|
export type MascotMood = "idle" | "blink" | "happy" | "thinking" | "tool" | "waiting" | "sleepy" | "error" | "complete" | "poke";
|
|
9
9
|
export type MascotPaint = (color: "accent" | "warning" | "error" | "muted" | "dim", text: string) => string;
|
|
10
10
|
|
|
11
11
|
const FACES: Record<MascotMood, string> = {
|
|
12
|
-
idle: "
|
|
13
|
-
waiting: "
|
|
12
|
+
idle: "▪▄▪", blink: "─▄─", happy: "▀▄▀", thinking: "▪─▪", tool: ">▄<",
|
|
13
|
+
waiting: "▪o▪", sleepy: "─_─", error: "×_×", complete: "✦▄✦", poke: "▀o▀"
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
15
|
+
/** Hand-authored pixel crowns; animation selects frames, never changes layout or allocates a canvas. */
|
|
16
16
|
const TIPS: Record<MascotMood, readonly string[]> = {
|
|
17
|
-
idle: ["
|
|
18
|
-
blink: ["
|
|
19
|
-
happy: ["
|
|
20
|
-
thinking: ["
|
|
21
|
-
tool: ["
|
|
22
|
-
waiting: ["
|
|
23
|
-
sleepy: ["
|
|
24
|
-
error: ["
|
|
25
|
-
complete: ["
|
|
26
|
-
poke: ["
|
|
17
|
+
idle: [" ▄█▄ ", " ▄▀█ ", " █▀▄ "],
|
|
18
|
+
blink: [" ▄█▄ "],
|
|
19
|
+
happy: [" ▄█▄ ", "▄ █ ▄"],
|
|
20
|
+
thinking: [" ▄█· ", " ▄█ ·", "· █▄ ", " ·█▄ "],
|
|
21
|
+
tool: [" ·█· ", " ▄█▄ ", "· █ ·"],
|
|
22
|
+
waiting: [" ▄█▄?", " ▄█▄ "],
|
|
23
|
+
sleepy: [" ▄zzz"],
|
|
24
|
+
error: [" ▄▀▄ ", " ▄▄▄ "],
|
|
25
|
+
complete: [" ·█· ", "· █ ·"],
|
|
26
|
+
poke: ["▄ █ ▄", " ▄█▄ "]
|
|
27
27
|
};
|
|
28
28
|
export const MASCOT_FACE_WIDTH = 5;
|
|
29
29
|
export const MASCOT_TIP_WIDTH = 5;
|
|
30
30
|
export const SLEEPY_AFTER_MS = 120_000;
|
|
31
|
+
export const MASCOT_FRAME_MS = 600;
|
|
31
32
|
const BLINK_MS = 180;
|
|
32
33
|
|
|
33
|
-
export function mascotFace(mood: MascotMood): string { return "
|
|
34
|
+
export function mascotFace(mood: MascotMood): string { return "▐" + FACES[mood] + "▌"; }
|
|
35
|
+
|
|
36
|
+
/** A static crown for the selected mood; motion-off must not consult mutable activity/timers. */
|
|
37
|
+
export function mascotTip(mood: MascotMood, frame = 0): string {
|
|
38
|
+
const tips = TIPS[mood];
|
|
39
|
+
return tips[((frame % tips.length) + tips.length) % tips.length]!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const MASCOT_SPRITE_WIDTH = 11;
|
|
43
|
+
export const MASCOT_SPRITE_HEIGHT = 10;
|
|
44
|
+
|
|
45
|
+
/** A fixed-width mood marker above the pet; an empty row keeps its position stable. */
|
|
46
|
+
export function mascotSpark(mood: MascotMood, frame = 0): string {
|
|
47
|
+
if (mood === "sleepy") return Math.abs(frame % 2) ? " zzz " : " zzz ";
|
|
48
|
+
if (mood === "complete") return Math.abs(frame % 2) ? " ✦ ✦ " : " ✦ ✦ ";
|
|
49
|
+
if (mood === "waiting") return " ? ";
|
|
50
|
+
return " ".repeat(MASCOT_SPRITE_WIDTH);
|
|
51
|
+
}
|
|
52
|
+
/** Original Ember artwork: a round, soft ember with a curled flame tuft. Dots are transparent. */
|
|
53
|
+
const BODY = [
|
|
54
|
+
".....o.....",
|
|
55
|
+
"....oho....",
|
|
56
|
+
"...ohhbo...",
|
|
57
|
+
"..ohhffbo..",
|
|
58
|
+
".ohfffffbo.",
|
|
59
|
+
"ohfffffffbo",
|
|
60
|
+
"ohfffffffbo",
|
|
61
|
+
"offfffffbso",
|
|
62
|
+
".osfffffso.",
|
|
63
|
+
"..ooooooo.."
|
|
64
|
+
] as const;
|
|
65
|
+
/** Outline, shadow, orange body, golden face, highlight, blush, eye, glint and mouth. */
|
|
66
|
+
export const EMBER_PALETTE: Readonly<Record<string, readonly [number, number, number]>> = {
|
|
67
|
+
o: [113, 42, 18], s: [198, 76, 24], b: [255, 139, 38], f: [255, 197, 79],
|
|
68
|
+
h: [255, 231, 154], c: [255, 128, 112], k: [58, 28, 20], w: [255, 249, 230], m: [196, 62, 52]
|
|
69
|
+
};
|
|
70
|
+
const EMBER_256: Readonly<Record<string, number>> = { o: 94, s: 166, b: 208, f: 221, h: 229, c: 210, k: 52, w: 230, m: 160 };
|
|
71
|
+
/** Left-eye 3×2 patches; the right eye mirrors them. Idle eyes are small 1×2 beans. */
|
|
72
|
+
const EYES: Record<MascotMood, readonly string[]> = {
|
|
73
|
+
idle: [".k.", ".k."], blink: ["...", ".k."],
|
|
74
|
+
happy: [".k.", "k.k"], thinking: ["..k", "..k"],
|
|
75
|
+
tool: ["kk.", ".k."], waiting: [".k.", ".k."],
|
|
76
|
+
sleepy: ["...", "kkk"], error: ["k..", ".k."],
|
|
77
|
+
complete: [".k.", "k.k"], poke: ["kkk", "..."]
|
|
78
|
+
};
|
|
79
|
+
const hopMood = (mood: MascotMood) => mood === "happy" || mood === "complete" || mood === "poke";
|
|
80
|
+
const poseFrame = (frame: number) => ((frame % 4) + 4) % 4;
|
|
81
|
+
const mirror = (rows: readonly string[]) => rows.map(row => [...row].reverse().join(""));
|
|
82
|
+
|
|
83
|
+
/** Pixel-space frames shared by the terminal renderer and the visual comparison artifact. */
|
|
84
|
+
export function mascotPixels(mood: MascotMood, frame = 0): string[] {
|
|
85
|
+
const grid = BODY.map(row => [...row]);
|
|
86
|
+
const stamp = (x: number, y: number, rows: readonly string[]) => {
|
|
87
|
+
rows.forEach((row, dy) => [...row].forEach((pixel, dx) => {
|
|
88
|
+
if (pixel !== "." && grid[y + dy]?.[x + dx] !== undefined) grid[y + dy]![x + dx] = pixel;
|
|
89
|
+
}));
|
|
90
|
+
};
|
|
91
|
+
const pose = poseFrame(frame);
|
|
92
|
+
// The tuft sways while active; the face stays put.
|
|
93
|
+
if (!["idle", "blink", "sleepy"].includes(mood) && pose % 2 === 1) {
|
|
94
|
+
for (let y = 0; y < 3; y++) grid[y] = [".", ...grid[y]!.slice(0, -1)];
|
|
95
|
+
}
|
|
96
|
+
// Thinking glances right, then left; both eyes look the same way.
|
|
97
|
+
const eyes = mood === "thinking" && pose >= 2 ? mirror(EYES.thinking) : EYES[mood];
|
|
98
|
+
const eyeRow = mood === "waiting" ? 4 : 5;
|
|
99
|
+
stamp(2, eyeRow, eyes);
|
|
100
|
+
stamp(6, eyeRow, mood === "thinking" ? eyes : mirror(eyes));
|
|
101
|
+
if (mood === "error") { stamp(1, 7, ["cc"]); stamp(8, 7, ["cc"]); }
|
|
102
|
+
else { stamp(2, 7, ["c"]); stamp(8, 7, ["c"]); }
|
|
103
|
+
stamp(5, 7, [hopMood(mood) ? "m" : "k"]);
|
|
104
|
+
if (mood === "complete") stamp(5, 1, ["w"]);
|
|
105
|
+
// Squash before a hop; pouting gives a small grounded huff. Never grow the canvas.
|
|
106
|
+
if ((hopMood(mood) && pose === 1) || (mood === "error" && pose % 2 === 1)) {
|
|
107
|
+
for (let y = 3; y > 0; y--) grid[y] = [...grid[y - 1]!];
|
|
108
|
+
grid[0] = Array(MASCOT_SPRITE_WIDTH).fill(".");
|
|
109
|
+
}
|
|
110
|
+
return grid.map(row => row.join(""));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Two square pixels per terminal cell, like pixel-pet: colored half-blocks, not text-face glyphs. */
|
|
114
|
+
export function mascotSprite(mood: MascotMood, frame = 0, truecolor = supportsTruecolor()): string[] {
|
|
115
|
+
const pixels = mascotPixels(mood, frame);
|
|
116
|
+
const color = (pixel: string, background: boolean) => truecolor
|
|
117
|
+
? `\x1b[${background ? 48 : 38};2;${EMBER_PALETTE[pixel]!.join(";")}m`
|
|
118
|
+
: `\x1b[${background ? 48 : 38};5;${EMBER_256[pixel]}m`;
|
|
119
|
+
const rows: string[] = [];
|
|
120
|
+
for (let y = 0; y < MASCOT_SPRITE_HEIGHT; y += 2) {
|
|
121
|
+
let row = "";
|
|
122
|
+
for (let x = 0; x < MASCOT_SPRITE_WIDTH; x++) {
|
|
123
|
+
const top = pixels[y]![x]!, bottom = pixels[y + 1]![x]!;
|
|
124
|
+
row += top === "." && bottom === "." ? " "
|
|
125
|
+
: top === "." ? color(bottom, false) + "▄\x1b[0m"
|
|
126
|
+
: bottom === "." ? color(top, false) + "▀\x1b[0m"
|
|
127
|
+
: color(top, false) + color(bottom, true) + "▀\x1b[0m";
|
|
128
|
+
}
|
|
129
|
+
rows.push(row);
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A hop uses the existing spark row as headroom, never moving the editor below it. */
|
|
135
|
+
export function mascotPerch(mood: MascotMood, frame = 0, paint?: MascotPaint, truecolor = supportsTruecolor()): string[] {
|
|
136
|
+
const sprite = mascotSprite(mood, frame, truecolor);
|
|
137
|
+
if (hopMood(mood) && poseFrame(frame) === 2) return [...sprite, " ".repeat(MASCOT_SPRITE_WIDTH)];
|
|
138
|
+
const spark = mascotSpark(mood, frame);
|
|
139
|
+
return [paint ? paint(mood === "sleepy" ? "dim" : "accent", spark) : spark, ...sprite];
|
|
140
|
+
}
|
|
34
141
|
|
|
35
142
|
export class Mascot {
|
|
36
143
|
private phase: WorkingPhase = "idle";
|
|
@@ -75,8 +182,7 @@ export class Mascot {
|
|
|
75
182
|
face(now = Date.now()): string { return mascotFace(this.mood(now)); }
|
|
76
183
|
|
|
77
184
|
tip(now = Date.now(), frame = 0): string {
|
|
78
|
-
|
|
79
|
-
return tips[((frame % tips.length) + tips.length) % tips.length]!;
|
|
185
|
+
return mascotTip(this.mood(now), frame);
|
|
80
186
|
}
|
|
81
187
|
|
|
82
188
|
/**
|
|
@@ -85,7 +191,10 @@ export class Mascot {
|
|
|
85
191
|
*/
|
|
86
192
|
nextTransitionDelay(now = Date.now()): number | undefined {
|
|
87
193
|
const current = this.mood(now);
|
|
88
|
-
if (this.flashMood && now < this.flashMood.until)
|
|
194
|
+
if (this.flashMood && now < this.flashMood.until) {
|
|
195
|
+
// Only transient reactions need timed pose frames while Pi is idle; stop on expiry.
|
|
196
|
+
return Math.max(16, Math.min(this.flashMood.until - now, MASCOT_FRAME_MS - now % MASCOT_FRAME_MS));
|
|
197
|
+
}
|
|
89
198
|
if (this.phase !== "idle") return undefined;
|
|
90
199
|
const sleepyAt = this.lastActive + SLEEPY_AFTER_MS;
|
|
91
200
|
if (now >= sleepyAt) return undefined;
|
|
@@ -94,7 +203,10 @@ export class Mascot {
|
|
|
94
203
|
}
|
|
95
204
|
|
|
96
205
|
/** Visual identity for change detection (avoids needless renders). */
|
|
97
|
-
key(now = Date.now(), frame =
|
|
206
|
+
key(now = Date.now(), frame = Math.floor(now / MASCOT_FRAME_MS)): string {
|
|
207
|
+
const mood = this.mood(now);
|
|
208
|
+
return mood + ":" + mascotTip(mood, frame) + ":" + mascotPerch(mood, frame).join("");
|
|
209
|
+
}
|
|
98
210
|
}
|
|
99
211
|
|
|
100
212
|
/** Colorize the face; truecolor terminals get the flame palette, others theme colors. */
|
|
@@ -109,8 +221,8 @@ export function paintTip(tip: string, mood: MascotMood, paint?: MascotPaint, tru
|
|
|
109
221
|
if (!truecolor) return paint ? paint(mood === "sleepy" ? "dim" : "warning", tip) : tip;
|
|
110
222
|
return [...tip].map((char) => {
|
|
111
223
|
if (char === " ") return char;
|
|
112
|
-
if (char === "
|
|
113
|
-
if (char === "
|
|
224
|
+
if (char === "█" || char === "▀") return rgb(mood === "error" ? FLAME_RAMP[5]! : FLAME_RAMP[8]!, char);
|
|
225
|
+
if (char === "·") return rgb(FLAME_RAMP[9]!, char);
|
|
114
226
|
if (char === "z" || char === "?") return rgb(FLAME_RAMP[7]!, char);
|
|
115
227
|
return rgb(mood === "sleepy" ? FLAME_RAMP[4]! : FLAME_RAMP[6]!, char);
|
|
116
228
|
}).join("");
|
package/src/profiles.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { isRoleName, normalizeSpec, parseRoleConfig, type RoleConfig } from "./model-roles.ts";
|
|
4
|
-
import { isMaxSubagents, defaultVisualSettings, loadVisualSettings,
|
|
4
|
+
import { isMaxSubagents, defaultVisualSettings, loadVisualSettings, type JarVisualSettings } from "./settings.ts";
|
|
5
5
|
import { ACCENT_NAMES } from "./accent.ts";
|
|
6
6
|
import { FOOTER_FIELDS } from "./footer-settings.ts";
|
|
7
7
|
import { ICON_SETS } from "./icons.ts";
|
|
@@ -41,9 +41,7 @@ function normalizeProfile(value: unknown): JarProfile | undefined {
|
|
|
41
41
|
|| !isPlainObject(settings.footer) || typeof settings.animations !== "boolean" || typeof settings.ui !== "boolean"
|
|
42
42
|
|| typeof settings.composer !== "boolean" || typeof settings.mascot !== "boolean" || typeof settings.suggestions !== "boolean"
|
|
43
43
|
|| typeof settings.advisor !== "boolean" || typeof settings.advisorGates !== "boolean"
|
|
44
|
-
|| (settings.contextDiet !== undefined && typeof settings.contextDiet !== "boolean")
|
|
45
44
|
|| (settings.cacheDiagnostics !== undefined && typeof settings.cacheDiagnostics !== "boolean")
|
|
46
|
-
|| (settings.contextBudget !== undefined && !parseContextBudget(settings.contextBudget))
|
|
47
45
|
|| (settings.readCache !== undefined && typeof settings.readCache !== "boolean") || !Number.isInteger(settings.goalRounds)
|
|
48
46
|
|| (settings.goalRounds as number) < 1 || (settings.goalRounds as number) > 50
|
|
49
47
|
|| !isMaxSubagents(settings.maxSubagents) || typeof settings.accent !== "string"
|
|
@@ -53,9 +51,10 @@ function normalizeProfile(value: unknown): JarProfile | undefined {
|
|
|
53
51
|
if (raw.name === DEFAULT_PROFILE && raw.id !== "default") return undefined;
|
|
54
52
|
if (raw.name !== DEFAULT_PROFILE && (typeof raw.id !== "string" || !/^profile-[a-z0-9-]{8,64}$/.test(raw.id))) return undefined;
|
|
55
53
|
for (const [role, spec] of Object.entries(raw.roles.roles)) if (!isRoleName(role) || typeof spec !== "string" || !normalizeSpec(spec)) return undefined;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// Removed context controls must not survive a profile switch or subsequent save.
|
|
55
|
+
const { contextDiet: _diet, contextBudget: _budget, ...supported } = settings;
|
|
56
|
+
return { id: raw.id as string, name: raw.name, theme: raw.theme, settings: { ...supported,
|
|
57
|
+
cacheDiagnostics: settings.cacheDiagnostics ?? true, readCache: settings.readCache ?? false } as unknown as JarVisualSettings, roles: parseRoleConfig(raw.roles) };
|
|
59
58
|
}
|
|
60
59
|
function atomicSave(file: string, document: ProfileDocument): string {
|
|
61
60
|
mkdirSync(join(file, ".."), { recursive: true });
|
package/src/settings-ui.ts
CHANGED
|
@@ -2,9 +2,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { popup } from "./popup.ts";
|
|
3
3
|
import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
4
|
import { FOOTER_FIELDS, type FooterField } from "./footer-settings.ts";
|
|
5
|
-
import { formatTokens } from "./context-budget.ts";
|
|
6
5
|
import { ICON_SETS } from "./icons.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { GOAL_ROUND_CHOICES, MAX_SUBAGENT_CHOICES, type JarAccent, type JarVisualSettings } from "./settings.ts";
|
|
8
7
|
import type { ProfileAction } from "./profile-ui.ts";
|
|
9
8
|
|
|
10
9
|
const LABELS: Record<FooterField, string> = {
|
|
@@ -41,11 +40,11 @@ export async function openJarSettings(
|
|
|
41
40
|
let width = 64;
|
|
42
41
|
const accents: JarAccent[] = ["follow", ...availableAccents.filter((name): name is JarAccent =>
|
|
43
42
|
name === "default" || ["gray", "pink", "teal", "azure", "violet", "amber"].includes(name))];
|
|
44
|
-
let choosing: "goalRounds" | "maxSubagents" |
|
|
45
|
-
const choices = () => choosing === "maxSubagents" ? MAX_SUBAGENT_CHOICES :
|
|
46
|
-
const fields = () => choosing ? choices().length : page === "footer" ? FOOTER_FIELDS.length : page === "pi" ?
|
|
43
|
+
let choosing: "goalRounds" | "maxSubagents" | undefined;
|
|
44
|
+
const choices = () => choosing === "maxSubagents" ? MAX_SUBAGENT_CHOICES : GOAL_ROUND_CHOICES;
|
|
45
|
+
const fields = () => choosing ? choices().length : page === "footer" ? FOOTER_FIELDS.length : page === "pi" ? 8 : page === "profiles" ? 2 : 7;
|
|
47
46
|
// Back on the row that opened the picker.
|
|
48
|
-
const finishChoice = () => { selected = choosing === "maxSubagents" ? 3 :
|
|
47
|
+
const finishChoice = () => { selected = choosing === "maxSubagents" ? 3 : 2; choosing = undefined; };
|
|
49
48
|
const piError = (error: unknown) => ctx.ui.notify("Could not change Pi setting: " + (error as Error).message, "error");
|
|
50
49
|
const explainPool = () => choosing === "maxSubagents" || (page === "pi" && !choosing && selected === 3);
|
|
51
50
|
const pageSize = () => Math.min(fields(), Math.max(4, (process.stdout.rows ?? 24) - (explainPool() ? 10 : 9)));
|
|
@@ -55,7 +54,6 @@ export async function openJarSettings(
|
|
|
55
54
|
const state = current();
|
|
56
55
|
if (choosing) {
|
|
57
56
|
if (choosing === "maxSubagents") update({ ...state, maxSubagents: MAX_SUBAGENT_CHOICES[index]! });
|
|
58
|
-
else if (choosing === "contextBudget") update({ ...state, contextBudget: { ...state.contextBudget, softTokens: CONTEXT_BUDGET_CHOICES[index]! } });
|
|
59
57
|
else update({ ...state, goalRounds: GOAL_ROUND_CHOICES[index]! });
|
|
60
58
|
finishChoice();
|
|
61
59
|
} else if (page === "profiles") {
|
|
@@ -78,17 +76,8 @@ export async function openJarSettings(
|
|
|
78
76
|
}
|
|
79
77
|
else if (index === 4) update({ ...state, advisor: !state.advisor });
|
|
80
78
|
else if (index === 5) update({ ...state, advisorGates: !state.advisorGates });
|
|
81
|
-
else if (index === 6) update({ ...state,
|
|
82
|
-
else if (index === 7) update({ ...state,
|
|
83
|
-
else if (index === 8) {
|
|
84
|
-
const at = CONTEXT_BUDGET_ACTIONS.indexOf(state.contextBudget.action);
|
|
85
|
-
update({ ...state, contextBudget: { ...state.contextBudget, action: CONTEXT_BUDGET_ACTIONS[(at + 1) % CONTEXT_BUDGET_ACTIONS.length]! } });
|
|
86
|
-
}
|
|
87
|
-
else if (index === 9) {
|
|
88
|
-
choosing = "contextBudget";
|
|
89
|
-
selected = Math.max(0, CONTEXT_BUDGET_CHOICES.indexOf(state.contextBudget.softTokens as never));
|
|
90
|
-
}
|
|
91
|
-
else if (index === 10) update({ ...state, readCache: !state.readCache });
|
|
79
|
+
else if (index === 6) update({ ...state, cacheDiagnostics: !state.cacheDiagnostics });
|
|
80
|
+
else if (index === 7) update({ ...state, readCache: !state.readCache });
|
|
92
81
|
} else if (index === 0) {
|
|
93
82
|
const at = Math.max(0, accents.indexOf(state.accent));
|
|
94
83
|
update({ ...state, accent: accents[(at + 1) % accents.length]! });
|
|
@@ -164,10 +153,9 @@ export async function openJarSettings(
|
|
|
164
153
|
+ (profiles ? " " + theme.fg(page === "profiles" ? "accent" : "muted", "[ Profiles ]") : ""));
|
|
165
154
|
const prefs = pi?.get();
|
|
166
155
|
const divider = theme.fg("dim", "├" + "─".repeat(Math.max(0, width - 2)) + "┤");
|
|
167
|
-
const
|
|
168
|
-
const chosen = choosing === "contextBudget" ? state.contextBudget.softTokens : choosing ? state[choosing] : undefined;
|
|
156
|
+
const chosen = choosing ? state[choosing] : undefined;
|
|
169
157
|
const allRows = choosing
|
|
170
|
-
? choices().map((choice, index) => row(index,
|
|
158
|
+
? choices().map((choice, index) => row(index, String(choice), chosen === choice ? "CURRENT" : "", true))
|
|
171
159
|
: page === "footer"
|
|
172
160
|
? FOOTER_FIELDS.map((field, index) => row(index, LABELS[field], state.footer[field] ? "ON" : "OFF", state.footer[field]))
|
|
173
161
|
: page === "pi" ? [
|
|
@@ -177,11 +165,8 @@ export async function openJarSettings(
|
|
|
177
165
|
row(3, "Max subagents (retained live pool)", String(state.maxSubagents), true),
|
|
178
166
|
row(4, "Advisor (jar_advisor, /advisor)", state.advisor ? "ON" : "OFF", state.advisor),
|
|
179
167
|
row(5, "Advisor gates (loops, repeated failures)", state.advisorGates ? "ON" : "OFF", state.advisorGates),
|
|
180
|
-
|
|
181
|
-
row(7, "
|
|
182
|
-
row(8, "Context budget (past the limit)", state.contextBudget.action.toUpperCase(), budgetOn),
|
|
183
|
-
row(9, "Context budget limit", formatTokens(state.contextBudget.softTokens), budgetOn),
|
|
184
|
-
row(10, "Repeated-read stub (unchanged re-reads)", state.readCache ? "ON" : "OFF", state.readCache)
|
|
168
|
+
row(6, "Cache diagnostics (explain cache breaks)", state.cacheDiagnostics ? "ON" : "OFF", state.cacheDiagnostics),
|
|
169
|
+
row(7, "Repeated-read stub (unchanged re-reads)", state.readCache ? "ON" : "OFF", state.readCache)
|
|
185
170
|
] : page === "profiles" ? [
|
|
186
171
|
row(0, "Switch profile…", profiles!.activeName(), true),
|
|
187
172
|
row(1, "Create profile…", "OPEN", true)
|
|
@@ -198,14 +183,12 @@ export async function openJarSettings(
|
|
|
198
183
|
const rows = allRows.slice(start, start + pageSize());
|
|
199
184
|
return [top, tabs, divider,
|
|
200
185
|
line(theme.fg("accent", choosing === "maxSubagents" ? " MAX SUBAGENTS · RETAINED LIVE POOL"
|
|
201
|
-
: choosing === "contextBudget" ? " CONTEXT BUDGET · SOFT LIMIT"
|
|
202
186
|
: choosing === "goalRounds" ? " GOAL AUTO ROUNDS" : page === "footer" ? " FOOTER VISIBILITY" : page === "pi" ? " PI & WORKFLOWS" : page === "profiles" ? " PROFILES" : " APPEARANCE & MOTION")),
|
|
203
187
|
line(theme.fg("dim", explainPool() ? " Live pool includes idle/paused subagents"
|
|
204
188
|
: choosing ? " Select a value; Enter/click to save, Esc to cancel"
|
|
205
189
|
: page === "footer" ? ` Footer fields ${start + 1}–${start + rows.length}/${fields()}`
|
|
206
|
-
: page === "pi" ? (selected ===
|
|
207
|
-
: selected ===
|
|
208
|
-
: selected === 10 ? " An unchanged re-read of the same range returns a short stub" : " Fullscreen mode enables clicks and copy-on-select")
|
|
190
|
+
: page === "pi" ? (selected === 6 ? " Names what changed when a call re-sent cached context (/cache-breaks)"
|
|
191
|
+
: selected === 7 ? " An unchanged re-read of the same range returns a short stub" : " Fullscreen mode enables clicks and copy-on-select")
|
|
209
192
|
: page === "profiles" ? " Switch only in a fresh, idle session; roles via /roles" : " Accent cycles through loaded themes")),
|
|
210
193
|
...rows,
|
|
211
194
|
line(theme.fg("dim", choosing ? " ↑↓: choose · Enter/click: save · Esc: cancel"
|
package/src/settings.ts
CHANGED
|
@@ -23,12 +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;
|
|
28
26
|
/** Explain prompt-cache breaks: which tool, system section or message changed when a call re-sent cached context. */
|
|
29
27
|
cacheDiagnostics: boolean;
|
|
30
|
-
/** Context-size guard: past softTokens, suggest /compact or /new, or compact at the next safe point. */
|
|
31
|
-
contextBudget: ContextBudget;
|
|
32
28
|
/** Opt-in: an unchanged repeated `read` of the same range returns a short stub instead of the content. */
|
|
33
29
|
readCache: boolean;
|
|
34
30
|
/** Glyph set for footer, composer and views: unicode, Nerd Font icons, or plain ascii. */
|
|
@@ -36,20 +32,6 @@ export interface JarVisualSettings {
|
|
|
36
32
|
footer: FooterSettings;
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
export const CONTEXT_BUDGET_ACTIONS = ["off", "suggest", "compact"] as const;
|
|
40
|
-
export type ContextBudgetAction = (typeof CONTEXT_BUDGET_ACTIONS)[number];
|
|
41
|
-
export interface ContextBudget { softTokens: number; action: ContextBudgetAction }
|
|
42
|
-
export const CONTEXT_BUDGET_CHOICES = [80_000, 120_000, 160_000, 200_000] as const;
|
|
43
|
-
export const DEFAULT_CONTEXT_BUDGET: ContextBudget = { softTokens: 120_000, action: "suggest" };
|
|
44
|
-
/** Accepts any stored integer budget in a sane range, not only the picker's choices. */
|
|
45
|
-
export function parseContextBudget(value: unknown): ContextBudget | undefined {
|
|
46
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
47
|
-
const { softTokens, action } = value as Record<string, unknown>;
|
|
48
|
-
if (typeof softTokens !== "number" || !Number.isInteger(softTokens) || softTokens < 10_000 || softTokens > 2_000_000) return undefined;
|
|
49
|
-
if (!CONTEXT_BUDGET_ACTIONS.some((choice) => choice === action)) return undefined;
|
|
50
|
-
return { softTokens, action: action as ContextBudgetAction };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
35
|
export const GOAL_ROUND_CHOICES = [4, 8, 12, 20] as const;
|
|
54
36
|
export const MAX_SUBAGENT_CHOICES = [2, 4, 6, 8, 16] as const;
|
|
55
37
|
export type MaxSubagents = (typeof MAX_SUBAGENT_CHOICES)[number];
|
|
@@ -59,7 +41,7 @@ export function isMaxSubagents(value: unknown): value is MaxSubagents {
|
|
|
59
41
|
|
|
60
42
|
export const SETTINGS_FILE = "pi-jar-settings.json";
|
|
61
43
|
export function defaultVisualSettings(footer: FooterSettings = DEFAULT_FOOTER_SETTINGS): JarVisualSettings {
|
|
62
|
-
return { version: 1, accent: "follow", animations: true, ui: true, composer: true, mascot: true, suggestions: true, goalRounds: 8, maxSubagents: 2, advisor: true, advisorGates: false,
|
|
44
|
+
return { version: 1, accent: "follow", animations: true, ui: true, composer: true, mascot: true, suggestions: true, goalRounds: 8, maxSubagents: 2, advisor: true, advisorGates: false, cacheDiagnostics: true, readCache: false, icons: "unicode", footer: { ...footer } };
|
|
63
45
|
}
|
|
64
46
|
|
|
65
47
|
/** Legacy footer choices are imported only while the new file is absent. Never modify the old file. */
|
|
@@ -92,9 +74,7 @@ export function loadVisualSettings(directory: string): JarVisualSettings {
|
|
|
92
74
|
maxSubagents: isMaxSubagents(value.maxSubagents) ? value.maxSubagents : fallback.maxSubagents,
|
|
93
75
|
advisor: typeof value.advisor === "boolean" ? value.advisor : fallback.advisor,
|
|
94
76
|
advisorGates: typeof value.advisorGates === "boolean" ? value.advisorGates : fallback.advisorGates,
|
|
95
|
-
contextDiet: typeof value.contextDiet === "boolean" ? value.contextDiet : fallback.contextDiet,
|
|
96
77
|
cacheDiagnostics: typeof value.cacheDiagnostics === "boolean" ? value.cacheDiagnostics : fallback.cacheDiagnostics,
|
|
97
|
-
contextBudget: parseContextBudget(value.contextBudget) ?? fallback.contextBudget,
|
|
98
78
|
readCache: typeof value.readCache === "boolean" ? value.readCache : fallback.readCache,
|
|
99
79
|
icons: ICON_SETS.some((set) => set === value.icons) ? value.icons as IconSet : fallback.icons,
|
|
100
80
|
footer: Object.fromEntries(FOOTER_FIELDS.map((field) => [field,
|
package/src/subagent-tools.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { workspacePathAllowed } from "./delegate-worktree.ts";
|
|
5
5
|
|
|
6
6
|
export const CHILD_TOOLS_ENV = "PI_JAR_CHILD_TOOLS";
|
|
7
|
+
export const CHILD_SKILL_READS_ENV = "PI_JAR_CHILD_SKILL_READS";
|
|
8
|
+
|
|
9
|
+
/** Trusted skill instructions and their relative references; never extension/package roots. */
|
|
10
|
+
export function skillReadRoots(paths: readonly string[]): string[] {
|
|
11
|
+
return [...new Set(paths.map(path => {
|
|
12
|
+
const canonical = realpathSync(path);
|
|
13
|
+
// Retain the trusted spelling: /var and /private/var (or a symlinked skill
|
|
14
|
+
// install) must both work. workspacePathAllowed resolves its physical boundary.
|
|
15
|
+
const spelling = resolve(path);
|
|
16
|
+
return statSync(canonical).isDirectory() || basename(path) !== "SKILL.md" || lstatSync(path).isSymbolicLink()
|
|
17
|
+
? spelling : dirname(spelling);
|
|
18
|
+
}))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Malformed inherited grants fail closed. Parent-owned roots are absolute; the guard resolves their canonical boundary. */
|
|
22
|
+
export function childSkillReadRoots(value: string | undefined): readonly string[] {
|
|
23
|
+
if (value === undefined) return [];
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(value);
|
|
26
|
+
if (!Array.isArray(parsed) || parsed.some(path => typeof path !== "string" || !isAbsolute(path) || path.includes("\0"))) return [];
|
|
27
|
+
return parsed;
|
|
28
|
+
} catch { return []; }
|
|
29
|
+
}
|
|
7
30
|
export const WEB_TOOLS = ["web_enable", "web_search", "fetch_content", "get_search_content", "source_check"] as const;
|
|
8
31
|
export const RECURSIVE_TOOLS = new Set(["jar_delegate", "jar_subagent", "jar_council"]);
|
|
9
32
|
export const SHELL_TOOLS = new Set(["bash", "powershell", "jar_shell"]);
|
|
@@ -71,14 +94,15 @@ export function loadSubagentConfig(directory: string, home = homedir()): Subagen
|
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
/** Multi-file tools must validate each effective target, not just the top-level default path. */
|
|
74
|
-
export function worktreeToolViolation(root: string, cwd: string, name: string, args: unknown): string | undefined {
|
|
97
|
+
export function worktreeToolViolation(root: string, cwd: string, name: string, args: unknown, skillRoots: readonly string[] = []): string | undefined {
|
|
75
98
|
if (SHELL_TOOLS.has(name)) return "Sandboxed worktree subagents cannot run shell tools.";
|
|
76
99
|
const pathTools = new Set(["read", "edit", "write", "grep", "find", "ls", "multi_file_edit"]);
|
|
77
100
|
if (!pathTools.has(name)) return;
|
|
78
101
|
if (!workspacePathAllowed(root, cwd, ".")) return "Sandboxed worktree subagent working directory is outside its workspace.";
|
|
79
102
|
const input = args && typeof args === "object" ? args as Record<string, unknown> : {};
|
|
80
103
|
const validate = (value: unknown): string | undefined => {
|
|
81
|
-
|
|
104
|
+
const grantedRead = name === "read" && typeof value === "string" && skillRoots.some(skill => workspacePathAllowed(skill, cwd, value, true));
|
|
105
|
+
if (typeof value !== "string" || !value || (!workspacePathAllowed(root, cwd, value, name === "read") && !grantedRead)) {
|
|
82
106
|
return "Sandboxed worktree subagent cannot access outside its workspace or use an invalid path: " + String(value);
|
|
83
107
|
}
|
|
84
108
|
};
|
package/src/task-tool.ts
CHANGED
|
@@ -90,7 +90,8 @@ export function registerTaskTool(
|
|
|
90
90
|
description: "Maintain session tasks. write sets the complete current plan (omitted tasks are removed; each item has content, status, optional activeForm and one level of `subtasks`, whose completion completes the parent; progress counts leaves). update changes one task by stable id, append adds genuinely new work mid-run (not a new plan), remove deletes one task tree; start/done/open/edit are concise status operations. Stable IDs are returned for every task.",
|
|
91
91
|
promptSnippet: "Track work with 3+ steps or several requests in jar_todo (skip trivial requests and questions): write the complete plan for a new request, start fresh once the old list is completed.",
|
|
92
92
|
promptGuidelines: [
|
|
93
|
-
"Keep exactly one leaf in_progress; mark it completed right after verifying it. Never batch completions or complete partial, failing or blocked work: keep it open and add a task for the blocker."
|
|
93
|
+
"Keep exactly one leaf in_progress; mark it completed right after verifying it. Never batch completions or complete partial, failing or blocked work: keep it open and add a task for the blocker.",
|
|
94
|
+
"After automatic compaction, resume unfinished authorized work from the tracked task state (use jar_todo list when needed). A compaction summary or background-job notification is not task completion. Respect the latest user instructions, explicit stops or pauses, and blockers."
|
|
94
95
|
],
|
|
95
96
|
parameters: Parameters,
|
|
96
97
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-amber",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#F7B955",
|
|
6
7
|
"blue": "#8DB9FF",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-azure",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#75BCFF",
|
|
6
7
|
"blue": "#8DB9FF",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-gray",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#D0D9E5",
|
|
6
7
|
"blue": "#8DB9FF",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-pink",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#FF88C6",
|
|
6
7
|
"blue": "#8DB9FF",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-teal",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#45DDD1",
|
|
6
7
|
"blue": "#8DB9FF",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark-violet",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#BCA0FF",
|
|
6
7
|
"blue": "#8DB9FF",
|
package/themes/pi-jar-dark.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
|
3
3
|
"name": "pi-jar-dark",
|
|
4
|
+
"appearance": "dark",
|
|
4
5
|
"vars": {
|
|
5
6
|
"accent": "#2DDED0",
|
|
6
7
|
"blue": "#8DB9FF",
|