pi-jar 0.1.9 → 0.2.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 +15 -5
- package/extensions/index.ts +152 -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 +11 -9
- package/src/model-roles.ts +29 -9
- package/src/profile-wizard.ts +61 -0
- package/src/profiles.ts +148 -0
- package/src/settings-ui.ts +46 -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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-jar
|
|
2
2
|
|
|
3
|
-
A warm, role-aware TUI and workflow extension for [Pi](https://github.com/earendil-works/pi). pi-jar turns the terminal into a more expressive agent workspace with a living pixel-fire welcome, an adaptive composer, first-class **plan**, **goal** and **role** workflows, tracked tasks, reviewable changes, background shells and parallel subagents.
|
|
3
|
+
A warm, role-aware TUI and workflow extension for [Pi](https://github.com/earendil-works/pi). pi-jar turns the terminal into a more expressive agent workspace with a living pixel-fire welcome, an adaptive composer, switchable **profiles**, first-class **plan**, **goal** and **role** workflows, tracked tasks, reviewable changes, background shells and parallel subagents.
|
|
4
4
|
|
|
5
5
|
> Works with the current `@earendil-works/*` Pi API. Pi's core packages are peer dependencies (`"*"`), so pi-jar always runs against the Pi you have installed; it is tested against the latest release (0.87.x).
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ The showcase is captured from a real pi-jar terminal session: the torch-style `
|
|
|
24
24
|
| **Commit** | `/jar commit [note]` drafts a message for the staged changes with the `commit` role, lets you edit it, then commits (never pushes). |
|
|
25
25
|
| **Tasks & questions** | A Claude/omp-style `jar_todo` checklist with subtasks and per-task progress that the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
|
|
26
26
|
| **Change review** | Every file the agent or its editing subagents change is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files (each counted once) next to a colored diff, with accept or revert per file or all at once. |
|
|
27
|
-
| **Background shells** | `jar_shell` runs dev servers, watchers and long tests in the background;
|
|
27
|
+
| **Background shells** | `jar_shell` runs dev servers, watchers and long tests in the background; watch/exit events wake an idle agent or reach it at the next safe turn boundary. Completion status appears immediately in the UI even during model reasoning. Running shells get a footer row; click it (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>) to tail or kill them. |
|
|
28
28
|
| **Subagents** | The main agent becomes a moderator over a configurable retained pool (2, 4, 6, 8 or 16; default 4). `jar_delegate` starts `scout`, `fork` or sandboxed `worktree` agents and returns immediately, so the main agent can keep responding to user steering; Pi RPC event-bus messages deliver initial and resumed turn completions without polling. `jar_subagent` peeks, steers, asks BTW questions, pauses, resumes and stops them. Workers can exchange bounded structured Q/A through `jar_discuss`; worktree changes stay isolated until stop, then safely enter `/diff`. |
|
|
29
29
|
| **Sessions & history** | Recent sessions (with their goal and plan) on the welcome card, one click from resuming; <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd> searches earlier prompts; pasted images show as chips under the composer. |
|
|
30
30
|
| **Footer & themes** | Responsive footer (model, effort, session, cwd, context, RAM, cost, quota, goal, roles, branch, live subagents and shells) with unicode, [Nerd Font](https://www.nerdfonts.com) or ascii icons, and seven dark themes. |
|
|
@@ -79,6 +79,7 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
|
|
|
79
79
|
| Shortcut | Action |
|
|
80
80
|
| --- | --- |
|
|
81
81
|
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> | Open pi-jar settings |
|
|
82
|
+
| <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> | Cycle profiles (theme and model roles) |
|
|
82
83
|
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd> | Subagents and background shells (activity view) |
|
|
83
84
|
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> | Refresh the welcome (new message and flame), or show it again |
|
|
84
85
|
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>P</kbd> | Toggle plan mode |
|
|
@@ -87,6 +88,14 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
|
|
|
87
88
|
| <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd> | Search earlier prompts into the composer |
|
|
88
89
|
| <kbd>Tab</kbd> / <kbd>→</kbd> in an empty composer | Accept the dim suggestion into the input (it is not sent) |
|
|
89
90
|
|
|
91
|
+
## Profiles
|
|
92
|
+
|
|
93
|
+
Profiles keep separate themes, appearance settings, and model-role configurations. **Default** preserves your existing setup.
|
|
94
|
+
|
|
95
|
+
Open `/jar settings` (or **Ctrl+Alt+S**) and choose **Profiles** to create or select a custom profile, or change its theme. The wizard lets you name it, choose its theme, configure roles, and review before saving. Cancelling leaves your existing setup unchanged.
|
|
96
|
+
|
|
97
|
+
Press **Ctrl+Shift+Tab** to cycle profiles **only in a fresh, idle session**. Once the first prompt is sent, the session's profile is locked—even after the agent finishes—and restored when you resume it. Open a new session to switch. Active agents, subagents, and plan/goal workflows also block switching. Configure the selected profile's roles through `/roles`; changing one profile does not change another. The selected profile is remembered across restarts and shown on the welcome screen and beside the composer mascot: `mascot · (Profile) session_name`. Some terminals require an extended keyboard protocol to distinguish Ctrl+Shift+Tab.
|
|
98
|
+
|
|
90
99
|
## Welcome screen
|
|
91
100
|
|
|
92
101
|
The welcome stays until your first prompt, then dissolves (or hides immediately with motion off).
|
|
@@ -115,7 +124,7 @@ The welcome stays until your first prompt, then dissolves (or hides immediately
|
|
|
115
124
|
| proud | `(★ᴗ★)` | a goal was completed |
|
|
116
125
|
| poked | `(^o^)` | you clicked it |
|
|
117
126
|
|
|
118
|
-
The title also shows Pi's session name (or a stable readable alias like `silver-lantern`)
|
|
127
|
+
The title also shows the active profile in parentheses, then Pi's session name (or a stable readable alias like `silver-lantern`): `mascot · (Default) silver-lantern`. Toggle the mascot in settings; `/jar composer off` restores Pi's editor with your draft intact.
|
|
119
128
|
|
|
120
129
|
## Plan mode
|
|
121
130
|
|
|
@@ -213,7 +222,7 @@ Roles map a purpose to a model. They live in `~/.pi/agent/pi-jar-roles.json` (Pi
|
|
|
213
222
|
|
|
214
223
|
The advisor is a second model (the `advisor` role; the current model if unassigned) that sees a fresh view of the recent conversation plus `git status` and a diff stat, but cannot run tools.
|
|
215
224
|
|
|
216
|
-
- **`jar_advisor({ question?, draft? })`** —
|
|
225
|
+
- **`jar_advisor({ question?, draft? })`** — starts a background second opinion and immediately returns a request ID. Use `action: "status"`, `"get"`, bounded `"wait"` (at most 30 seconds), or `"cancel"` with that ID; a pending receipt is **not** reviewed approval. Advice arrives at a safe model boundary or wakes an idle agent, with immediate UI completion feedback. Requests have a 60-second deadline and bounded context; `/advisor` and automatic stuck-work gates are non-blocking too. Get completed advice before consequential decisions. Available in plan mode too.
|
|
217
226
|
- **`/advisor [focus]`** — ask on demand; the answer is added to the conversation (visible) for the agent's next turn.
|
|
218
227
|
- **Gates** — when the agent makes the same tool call three times within its last eight calls, the call is blocked and the advisor's review is returned instead; after three failing tool results in a row, the advice is steered into the running turn. At most two automatic consultations per prompt.
|
|
219
228
|
- Settings → Pi toggles the advisor and the gates. Advisor calls are counted in `/usage`. The footer and welcome show it as a working teammate while it thinks.
|
|
@@ -256,7 +265,8 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
|
|
|
256
265
|
Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
|
|
257
266
|
- **`jar_ask`** — structured questions: numbered options with descriptions, single or multi-select, *Type your own answer* (multi-line, paste-friendly) and *Chat about this* to discuss before choosing.
|
|
258
267
|
- **`/diff`** — review what the agent (and its editing subagents) changed since the first edit to each file: files with `+/−` counts on the left, a full-path header and numbered, colored diff on the right. Changed lines get a colored rail and tinted background, replaced lines highlight the changed words, code is syntax-highlighted for known languages, and collapsed regions are labeled `┄ N unchanged lines ┄`. `/` filters files; `t` switches unified and aligned side-by-side views (unified below 70 columns), `←`/`→` (or `[`/`]`) change the context from 0 lines to the whole file, `w` wraps long lines, and `n`/`p`, `Home`/`End` navigate. Large changes offer summary or explicit windowed review (`v`), with clearly labeled coarse fallback and existing safety limits—not arbitrary-file streaming. See [diff review](docs/DIFF-REVIEW.md). `a` accept (keep, stop tracking), `r` then `y` revert (restore the original, or remove a file it created), `A`/`R` for all. The footer shows `± N files · /diff` while anything is unreviewed; each file counts once no matter who edited it. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
|
|
259
|
-
-
|
|
268
|
+
- **Context diet (opt-in)** — `/jar settings` → **Pi** → **Context diet (prior-turn reasoning)**. Saved independently per profile; OFF by default. Removes eligible thinking blocks from completed earlier user turns only in outgoing context, never from stored history. Current reasoning signatures, user requests, tool arguments/results, and workflow state remain intact; ambiguous tool chains are left unchanged. Opaque signature character counts are diagnostics, not token savings. Existing SoL-Pi observation packing remains responsible for tool-result projection.
|
|
269
|
+
- **`jar_shell`** — `start` (optional `name`, `watch` regex, `notify`, task/service `purpose`), `list`, `peek`, bounded `wait`, `output`, `kill`. ANSI-stripped logs have per-job caps and a 4 MiB aggregate budget; surfaced finished jobs compact to a 32 KiB tail. Live jobs retain useful tails and are never killed to reclaim logs. Watch/exit notifications are compact and coalesced; reads acknowledge observed events. Active model responses cannot be interrupted by a completion event: the UI previews status without consuming it, and model delivery waits for a safe boundary. When no useful work remains, use bounded `wait` for the relevant finite jobs rather than waiting for an automatic wake-up. Each running shell gets a footer row and an activity view (`/jar shells`) with `x` kill and `f` follow. All shells stop when the session ends.
|
|
260
270
|
- **`jar_delegate`** — spawns retained, session-scoped subagents up to **Max subagents** in `/jar settings` (2, 4, 6, 8 or 16; default 4). Idle, paused and hibernated agents count toward this pool; over-cap batches are rejected. `scout` is fresh/read-only, `fork` inherits conversation read-only, and `worktree` forks context into a path-guarded Git worktree. Settled retained agents close their Pi process after 30 seconds of quiet while keeping their private session and workspace; `resume`/`ask` relaunch that same session. Launch returns before startup or the first turn completes, including legacy one-shot `write: true` mode.
|
|
261
271
|
- **`jar_subagent`** — `peek` reports progress, including the workspace path and changed files for recovery records; `steer` redirects a running worker; `resume` relaunches an idle/paused/hibernated agent. `ask`, `pause` and `stop` return accepted receipts immediately; answers and final handoffs arrive as `pi-jar.subagent` events. `stop` reconciles safe worktree changes; conflicts remain private and can be retried with `stop` after resolving parent drift. `discard` removes an unresolved recovery worktree without applying it. At most four active/recovery worktrees are admitted; resolve or discard one before starting another. Session shutdown reports paths of any preserved worktrees. Controls do not poll the child.
|
|
262
272
|
- **Subagent capabilities** — each task can set `tools` or `inheritTools: true`; known-tool mode filters and child guards remain in force. See [tool capabilities](docs/SUBAGENT-TOOLS.md).
|
package/extensions/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAgentDir, SessionManager, SettingsManager, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { basename } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { Key, truncateToWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
@@ -17,12 +17,14 @@ import { GOAL_ENTRY, GoalStore } from "../src/goals.ts";
|
|
|
17
17
|
import { GoalLoop } from "../src/goal-loop.ts";
|
|
18
18
|
import { FOOTER_FIELDS } from "../src/footer-settings.ts";
|
|
19
19
|
import { defaultVisualSettings, loadVisualSettings, saveVisualSettings, type JarVisualSettings } from "../src/settings.ts";
|
|
20
|
+
import { ProfileStore, PROFILE_FILE, PROFILE_ENTRY, conversationStarted, pinnedProfileId } from "../src/profiles.ts";
|
|
20
21
|
import { openJarSettings, type PiPreferences } from "../src/settings-ui.ts";
|
|
21
22
|
import { pickSession } from "../src/session-ui.ts";
|
|
22
23
|
import { fetchQuota, QuotaCache, type QuotaProvider } from "../src/quota.ts";
|
|
23
24
|
import { ModelRoleManager } from "../src/model-roles.ts";
|
|
24
25
|
import { jarCommit } from "../src/commit.ts";
|
|
25
26
|
import { registerAdvisor } from "../src/advisor.ts";
|
|
27
|
+
import { registerContextDiet } from "../src/context-diet.ts";
|
|
26
28
|
import { registerInfoPanels } from "../src/info-panels.ts";
|
|
27
29
|
import { SideUsage } from "../src/side-model.ts";
|
|
28
30
|
import { PlanMode } from "../src/plan.ts";
|
|
@@ -97,6 +99,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
97
99
|
installCompactBuiltinTools(pi);
|
|
98
100
|
let demo = false;
|
|
99
101
|
let visualSettings = defaultVisualSettings();
|
|
102
|
+
let profileStore: ProfileStore | undefined;
|
|
100
103
|
let animations = visualSettings.animations;
|
|
101
104
|
let enabled = visualSettings.ui;
|
|
102
105
|
let disposeFooter: (() => void) | undefined;
|
|
@@ -303,10 +306,52 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
303
306
|
registerAskTool(pi);
|
|
304
307
|
const modelRoles = new ModelRoleManager(pi);
|
|
305
308
|
const sideUsage = new SideUsage();
|
|
306
|
-
|
|
309
|
+
registerContextDiet(pi, () => visualSettings.contextDiet === true);
|
|
310
|
+
const advisor = registerAdvisor(pi, modelRoles, { enabled: () => !delegatedChild && visualSettings.advisor, gates: () => !delegatedChild && visualSettings.advisorGates, usage: sideUsage });
|
|
307
311
|
registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
|
|
308
312
|
quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
|
|
309
|
-
|
|
313
|
+
let followingTheme: string | undefined;
|
|
314
|
+
const profileEntries = (ctx: ExtensionContext): readonly unknown[] => {
|
|
315
|
+
try { return ctx.sessionManager.getEntries?.() ?? sessionBranch(ctx); } catch { return sessionBranch(ctx); }
|
|
316
|
+
};
|
|
317
|
+
const applyProfileTheme = (name: string, ctx: ExtensionContext) => {
|
|
318
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || typeof ctx.ui.setTheme !== "function") return;
|
|
319
|
+
const target = name === "follow" ? followingTheme ?? "system" : name;
|
|
320
|
+
if (!ctx.ui.getTheme?.(target)) throw new Error(`Theme ${target} is not loaded`);
|
|
321
|
+
const result = ctx.ui.setTheme(target);
|
|
322
|
+
if (result && !result.success) throw new Error(result.error ?? `Could not apply theme ${target}`);
|
|
323
|
+
};
|
|
324
|
+
const initializeProfiles = (ctx: ExtensionContext) => {
|
|
325
|
+
const previousTheme = ctx.ui.theme?.name;
|
|
326
|
+
followingTheme ??= previousTheme ?? "system";
|
|
327
|
+
try {
|
|
328
|
+
if (!existsSync(join(getAgentDir(), PROFILE_FILE))
|
|
329
|
+
&& (!ctx.hasUI || ctx.mode !== "tui" || typeof ctx.ui.getAllThemes !== "function")) {
|
|
330
|
+
profileStore = undefined; visualSettings = loadVisualSettings(getAgentDir()); return undefined;
|
|
331
|
+
}
|
|
332
|
+
modelRoles.load(ctx.cwd);
|
|
333
|
+
const legacyAccent = loadVisualSettings(getAgentDir()).accent;
|
|
334
|
+
const legacyTheme = accentTheme(legacyAccent);
|
|
335
|
+
const initialTheme = legacyAccent !== "follow" && ctx.ui.getTheme?.(legacyTheme) ? legacyTheme : previousTheme ?? "system";
|
|
336
|
+
const store = new ProfileStore(getAgentDir(), initialTheme, modelRoles.profileRoleConfig());
|
|
337
|
+
const entries = profileEntries(ctx);
|
|
338
|
+
const id = pinnedProfileId(entries) ?? (conversationStarted(entries) ? "default" : store.active().id);
|
|
339
|
+
const profile = store.list().find((item) => item.id === id);
|
|
340
|
+
if (!profile) throw new Error("This session's profile is no longer available");
|
|
341
|
+
applyProfileTheme(profile.theme, ctx);
|
|
342
|
+
if (store.activeName !== profile.name) store.activate(profile.name);
|
|
343
|
+
profileStore = store;
|
|
344
|
+
visualSettings = profile.settings;
|
|
345
|
+
composer.setProfile(profile.name);
|
|
346
|
+
return { roles: profile.roles, profileId: profile.id };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (previousTheme) { try { applyProfileTheme(previousTheme, ctx); } catch {} }
|
|
349
|
+
profileStore = undefined; visualSettings = loadVisualSettings(getAgentDir()); composer.setProfile("Default");
|
|
350
|
+
ctx.ui.notify(`Could not load profiles: ${(error as Error).message}`, "warning");
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
modelRoles.register((ctx) => openRolesUi(ctx, modelRoles), { activateDefault: () => !delegatedChild, beforeSessionStart: initializeProfiles });
|
|
310
355
|
let delegateController: DelegateController | undefined;
|
|
311
356
|
registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges,
|
|
312
357
|
discussionEnv: (key, name) => discussion?.childEnv(key, name),
|
|
@@ -478,7 +523,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
478
523
|
const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, Date.now()) : undefined;
|
|
479
524
|
const open = todos?.all().filter((item) => !item.done) ?? [];
|
|
480
525
|
const lines = welcomeLines(width, welcomeFrame, (color, text) => (ctx.ui.theme ?? theme).fg(color, text), {
|
|
481
|
-
...info, settingsClickable: tui.mode !== "regular", roles: live.roles,
|
|
526
|
+
...info, model: ctx.model?.id, profile: profileStore?.activeName ?? "Default", settingsClickable: tui.mode !== "regular", roles: live.roles,
|
|
482
527
|
quota: quota?.week?.used ?? quota?.fiveHour?.used,
|
|
483
528
|
effort: ctx.model?.reasoning === false ? "off" : pi.getThinkingLevel?.(),
|
|
484
529
|
...(modelRoles.activeRole() ? { activeRole: modelRoles.activeRole()! } : {}),
|
|
@@ -612,11 +657,26 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
612
657
|
}
|
|
613
658
|
};
|
|
614
659
|
|
|
615
|
-
const
|
|
660
|
+
const persistVisualPreferences = (next: JarVisualSettings, ctx: ExtensionContext, theme?: string) => {
|
|
661
|
+
if (profileStore) {
|
|
662
|
+
try { profileStore.update(profileStore.activeName, { settings: next, ...(theme ? { theme } : {}) }); }
|
|
663
|
+
catch (error) { ctx.ui.notify("Profile preferences changed for this session but could not be saved: " + (error as Error).message, "warning"); return; }
|
|
664
|
+
}
|
|
665
|
+
if (!profileStore || profileStore.active().id === "default") {
|
|
666
|
+
try { saveVisualSettings(getAgentDir(), next); }
|
|
667
|
+
catch { ctx.ui.notify("Visual preferences changed for this session but could not be saved", "warning"); }
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
const accentTheme = (accent: JarVisualSettings["accent"]) => accent === "follow" ? "follow" : accent === "default" ? "pi-jar-dark" : `pi-jar-dark-${accent}`;
|
|
671
|
+
const applyVisualSettings = (next: JarVisualSettings, ctx: ExtensionContext, options: { persist?: boolean; accent?: boolean } = {}) => {
|
|
616
672
|
const wasEnabled = enabled;
|
|
617
673
|
const hadMotion = animations;
|
|
674
|
+
const accentChanged = next.accent !== visualSettings.accent;
|
|
618
675
|
const quotaShown = !!footerTui && footerSettings.quota;
|
|
619
|
-
if (
|
|
676
|
+
if (options.accent !== false && accentChanged && next.accent === "follow") {
|
|
677
|
+
try { applyProfileTheme("follow", ctx); } catch (error) { ctx.ui.notify(String(error), "warning"); return; }
|
|
678
|
+
}
|
|
679
|
+
if (options.accent !== false && accentChanged && next.accent !== "follow" && !selectAccent(ctx, next.accent)) {
|
|
620
680
|
ctx.ui.notify(`Accent ${next.accent} is not loaded`, "warning");
|
|
621
681
|
return;
|
|
622
682
|
}
|
|
@@ -646,15 +706,75 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
646
706
|
welcomeTui?.requestRender();
|
|
647
707
|
// Quota shown again (field or UI back on): hiding it stopped lookups and dropped the cache.
|
|
648
708
|
if (!quotaShown) refreshQuota(ctx);
|
|
649
|
-
|
|
650
|
-
catch { ctx.ui.notify("Visual preferences changed for this session but could not be saved", "warning"); }
|
|
709
|
+
if (options.persist !== false) persistVisualPreferences(next, ctx, accentChanged ? accentTheme(next.accent) : undefined);
|
|
651
710
|
};
|
|
652
711
|
|
|
712
|
+
let switchingProfile = false;
|
|
713
|
+
const canSwitchProfile = (ctx: ExtensionContext) => {
|
|
714
|
+
if (switchingProfile || advisor.isBusy() || !ctx.isIdle?.() || working.phase !== "idle" || planMode.isEnabled() || goals?.isActive() || subagents.running() > 0) {
|
|
715
|
+
ctx.ui.notify("Finish the active agent/workflow before switching profiles", "warning"); return false;
|
|
716
|
+
}
|
|
717
|
+
if (conversationStarted(profileEntries(ctx)) || pinnedProfileId(profileEntries(ctx)) !== undefined) {
|
|
718
|
+
ctx.ui.notify("This session's profile is locked. Open a new session to switch profiles", "warning"); return false;
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
};
|
|
722
|
+
const switchProfile = async (name: string, ctx: ExtensionContext) => {
|
|
723
|
+
if (!profileStore || name === profileStore.activeName || !canSwitchProfile(ctx)) return;
|
|
724
|
+
const store = profileStore;
|
|
725
|
+
const previous = store.active();
|
|
726
|
+
const previousTheme = ctx.ui.theme?.name;
|
|
727
|
+
const previousModel = ctx.model;
|
|
728
|
+
const previousRole = modelRoles.activeRole();
|
|
729
|
+
const previousThinking = pi.getThinkingLevel();
|
|
730
|
+
const next = store.list().find((profile) => profile.name === name);
|
|
731
|
+
if (!next) { ctx.ui.notify("Unknown profile: " + name, "warning"); return; }
|
|
732
|
+
switchingProfile = true;
|
|
733
|
+
try {
|
|
734
|
+
applyProfileTheme(next.theme, ctx);
|
|
735
|
+
store.activate(name);
|
|
736
|
+
modelRoles.invalidateTemporaryRestores();
|
|
737
|
+
modelRoles.load(ctx.cwd, next.roles, next.id);
|
|
738
|
+
composer.setProfile(next.name);
|
|
739
|
+
applyVisualSettings(next.settings, ctx, { persist: false, accent: false });
|
|
740
|
+
if (!await modelRoles.activate("default", ctx, true)) ctx.ui.notify("Profile loaded, but its default model is unavailable; keeping the current model", "warning");
|
|
741
|
+
repaint("profile"); welcomeTui?.requestRender();
|
|
742
|
+
ctx.ui.notify(`Profile: ${next.name}`, "info");
|
|
743
|
+
} catch (error) {
|
|
744
|
+
try { if (store.activeName !== previous.name) store.activate(previous.name); } catch {}
|
|
745
|
+
if (previousTheme) { try { applyProfileTheme(previousTheme, ctx); } catch {} }
|
|
746
|
+
modelRoles.load(ctx.cwd, previous.roles, previous.id);
|
|
747
|
+
try {
|
|
748
|
+
if (previousModel && ctx.model !== previousModel && !await pi.setModel(previousModel)) throw new Error("Previous model is unavailable");
|
|
749
|
+
pi.setThinkingLevel(previousThinking);
|
|
750
|
+
modelRoles.restoreActiveRole(previousRole, ctx);
|
|
751
|
+
} catch { ctx.ui.notify("Could not restore the previous model after the failed profile switch", "warning"); }
|
|
752
|
+
composer.setProfile(previous.name);
|
|
753
|
+
applyVisualSettings(previous.settings, ctx, { persist: false, accent: false });
|
|
754
|
+
ctx.ui.notify(`Could not switch profile: ${(error as Error).message}`, "warning");
|
|
755
|
+
} finally { switchingProfile = false; }
|
|
756
|
+
};
|
|
653
757
|
openSettings = async (ctx) => {
|
|
654
758
|
if (settingsOpen) return;
|
|
655
759
|
settingsOpen = true;
|
|
656
760
|
try { await openJarSettings(ctx, () => visualSettings,
|
|
657
|
-
(next) => applyVisualSettings(next, ctx), loadedAccents(ctx), piPreferences(ctx)
|
|
761
|
+
(next) => applyVisualSettings(next, ctx), loadedAccents(ctx), piPreferences(ctx), profileStore ? {
|
|
762
|
+
store: profileStore, themes: ctx.ui.getAllThemes?.().map((theme) => theme.name) ?? [],
|
|
763
|
+
roles: () => modelRoles.profileRoleConfig(), roleManager: modelRoles,
|
|
764
|
+
create: (name, theme, settings, roles) => { const profile = profileStore!.add(name, theme, { ...settings, accent: "follow" }, roles); ctx.ui.notify(`Created profile ${name}; select it in a fresh session`, "info"); return profile; },
|
|
765
|
+
select: (name) => switchProfile(name, ctx),
|
|
766
|
+
setTheme: (name) => {
|
|
767
|
+
const previousTheme = ctx.ui.theme?.name;
|
|
768
|
+
const next = { ...visualSettings, accent: "follow" as const };
|
|
769
|
+
try { applyProfileTheme(name, ctx); profileStore!.update(profileStore!.activeName, { theme: name, settings: next }); }
|
|
770
|
+
catch (error) { if (previousTheme) applyProfileTheme(previousTheme, ctx); throw error; }
|
|
771
|
+
applyVisualSettings(next, ctx, { persist: false, accent: false });
|
|
772
|
+
if (profileStore!.active().id === "default") {
|
|
773
|
+
try { saveVisualSettings(getAgentDir(), next); }
|
|
774
|
+
catch { ctx.ui.notify("Theme saved, but legacy Default preferences could not be updated", "warning"); }
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} : undefined); }
|
|
658
778
|
finally { settingsOpen = false; }
|
|
659
779
|
};
|
|
660
780
|
|
|
@@ -675,7 +795,8 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
675
795
|
};
|
|
676
796
|
pi.on("session_start", (_event, ctx) => {
|
|
677
797
|
sideUsage.clear();
|
|
678
|
-
|
|
798
|
+
if (!profileStore) initializeProfiles(ctx);
|
|
799
|
+
composer.setProfile(profileStore?.activeName ?? "Default");
|
|
679
800
|
setIconSet(visualSettings.icons);
|
|
680
801
|
animations = visualSettings.animations;
|
|
681
802
|
enabled = visualSettings.ui;
|
|
@@ -743,7 +864,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
743
864
|
composer.setMascot(visualSettings.mascot);
|
|
744
865
|
if (visualSettings.composer && enabled) composer.enable(ctx);
|
|
745
866
|
suggest.sync();
|
|
746
|
-
if (visualSettings.accent !== "follow") selectAccent(ctx, visualSettings.accent);
|
|
867
|
+
if (!profileStore && visualSettings.accent !== "follow") selectAccent(ctx, visualSettings.accent);
|
|
747
868
|
if (branch.length >= LARGE_SESSION_ENTRIES) {
|
|
748
869
|
try {
|
|
749
870
|
const prefs = piPreferences(ctx)?.get();
|
|
@@ -778,11 +899,20 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
778
899
|
welcomeTui.requestRender();
|
|
779
900
|
};
|
|
780
901
|
pi.on("input", (event, ctx) => {
|
|
902
|
+
if (profileStore && !conversationStarted(profileEntries(ctx))) {
|
|
903
|
+
const id = profileStore.active().id;
|
|
904
|
+
if (pinnedProfileId(profileEntries(ctx)) !== id) pi.appendEntry?.(PROFILE_ENTRY, { version: 1, id });
|
|
905
|
+
}
|
|
781
906
|
if (event.source !== "interactive") return;
|
|
782
907
|
dismissWelcome(ctx);
|
|
783
908
|
if (!todosAcknowledged && todos?.all().every((item) => item.done)) { todosAcknowledged = true; updateTaskWidget(ctx); }
|
|
784
909
|
});
|
|
785
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
910
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
911
|
+
if (profileStore && pinnedProfileId(profileEntries(ctx)) !== profileStore.active().id) {
|
|
912
|
+
pi.appendEntry?.(PROFILE_ENTRY, { version: 1, id: profileStore.active().id });
|
|
913
|
+
}
|
|
914
|
+
dismissWelcome(ctx); working.start(); applyWorking(ctx); refreshQuota(ctx);
|
|
915
|
+
});
|
|
786
916
|
pi.on("turn_start", (_event, ctx) => { working.start(); applyWorking(ctx); });
|
|
787
917
|
pi.on("message_end", (event, ctx) => {
|
|
788
918
|
// Every message (user, assistant, tool result) moves the context window, and a tool-heavy turn
|
|
@@ -882,6 +1012,15 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
882
1012
|
if (picked !== undefined) ctx.ui.setEditorText(picked);
|
|
883
1013
|
}
|
|
884
1014
|
});
|
|
1015
|
+
pi.registerShortcut?.(Key.ctrlShift("tab"), {
|
|
1016
|
+
description: "Switch pi-jar profile",
|
|
1017
|
+
handler: async (ctx) => {
|
|
1018
|
+
if (!ctx.hasUI || ctx.mode !== "tui" || !profileStore || settingsOpen) return;
|
|
1019
|
+
const profiles = profileStore.list();
|
|
1020
|
+
const index = profiles.findIndex((profile) => profile.name === profileStore!.activeName);
|
|
1021
|
+
if (profiles.length > 1) await switchProfile(profiles[(index + 1) % profiles.length]!.name, ctx);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
885
1024
|
pi.registerShortcut?.(Key.ctrlAlt("m"), {
|
|
886
1025
|
description: "Cycle pi-jar model roles",
|
|
887
1026
|
handler: async (ctx) => { await modelRoles.cycle(ctx); repaint("roles"); }
|
|
@@ -997,8 +1136,7 @@ export default function piJar(pi: ExtensionAPI): void {
|
|
|
997
1136
|
const applied = selectAccent(ctx, selected);
|
|
998
1137
|
if (applied) {
|
|
999
1138
|
visualSettings = { ...visualSettings, accent: selected as JarVisualSettings["accent"] };
|
|
1000
|
-
|
|
1001
|
-
catch { ctx.ui.notify("Accent changed for this session but could not be saved", "warning"); }
|
|
1139
|
+
persistVisualPreferences(visualSettings, ctx, accentTheme(visualSettings.accent));
|
|
1002
1140
|
applyWorking(ctx);
|
|
1003
1141
|
}
|
|
1004
1142
|
const valid = selected === "default" || ACCENT_NAMES.some((name) => name === selected);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jar",
|
|
3
|
-
"version": "0.
|
|
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.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A warm, role-aware Pi terminal workspace with switchable profiles, animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|