gentle-pi 2.6.4 → 3.0.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 +25 -7
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +41 -19
- package/docs/readme-reference.md +175 -36
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +289 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +123 -97
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +74 -113
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +19 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +88 -0
- package/lib/session-changes.ts +147 -0
- package/lib/shell-bar.ts +24 -10
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-changes.ts +5 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +18 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +2 -3
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +321 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +883 -356
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +338 -205
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-capability-contract.test.ts +15 -1
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +119 -31
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +86 -0
- package/tests/session-changes-shell.test.ts +38 -0
- package/tests/session-changes.test.ts +114 -0
- package/tests/shell-bar.test.ts +35 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { CustomEditor, keyHint, type ExtensionAPI, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { execFile, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { profilesFilePath, readProfilesFileResult } from "../lib/agent-profiles.ts";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { renderShellBar, renderShellSidebarBar, shellEnabled, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
|
|
8
|
-
import { CHANGE_STATUS,
|
|
9
|
+
import { CHANGE_STATUS, renderChangesWidget, type ChangedFile, type ChangesModel, type GitRunner, type WorktreeChanges } from "../lib/shell-changes.ts";
|
|
9
10
|
import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
|
|
10
|
-
import { SessionWorktreeRegistry,
|
|
11
|
+
import { SessionWorktreeRegistry, resolveSessionWorktree, worktreeGitEnvironment, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
11
12
|
import { CARD_TONE, renderCard, type Card, type CardTheme } from "../lib/shell-card.ts";
|
|
13
|
+
import { CommandPalette, commandsKey, type CommandPaletteResult } from "../lib/command-palette.ts";
|
|
14
|
+
import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
|
|
15
|
+
import { agentsViewKey } from "../lib/agents-keys.ts";
|
|
12
16
|
import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
|
|
13
|
-
import { framePromptLines, PROMPT_HINT, PROMPT_STATE, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
|
|
17
|
+
import { framePromptLines, PROMPT_HINT, PROMPT_STATE, SHELL_PULSE_MS, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
|
|
14
18
|
import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, parseCodexUsage, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
|
|
15
19
|
import { UsageView } from "../lib/shell-usage-view.ts";
|
|
16
20
|
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
17
21
|
import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
22
|
+
import { SessionChanges, SESSION_CHANGE_EVENT } from "../lib/session-changes.ts";
|
|
23
|
+
import { installSessionChangeCapture } from "../lib/session-change-capture.ts";
|
|
18
24
|
|
|
19
25
|
// Gentle Shell: the visual layer gentle-pi puts on top of pi. It installs the
|
|
20
26
|
// status bar, the petal prompt, the working-tree changes widget and overlay,
|
|
@@ -39,6 +45,7 @@ interface ShellBarComponent {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
interface BuildOptions {
|
|
48
|
+
profile?: string;
|
|
42
49
|
home?: string;
|
|
43
50
|
dirty?: number;
|
|
44
51
|
usage?: ProviderUsage;
|
|
@@ -47,6 +54,7 @@ interface BuildOptions {
|
|
|
47
54
|
export type DevBinaryNotice = { state: "active"; path: string; sha256: string } | { state: "invalid"; reason: string };
|
|
48
55
|
|
|
49
56
|
export interface ShellDeps {
|
|
57
|
+
activeProfile(): string | undefined;
|
|
50
58
|
fetch: typeof fetch;
|
|
51
59
|
now(): number;
|
|
52
60
|
devBinary(): DevBinaryNotice | undefined;
|
|
@@ -54,6 +62,31 @@ export interface ShellDeps {
|
|
|
54
62
|
gitRunner(cwd: string): GitRunner;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
// The rail digest runs every frame. Cache parsing by file identity and metadata,
|
|
66
|
+
// not just mtime: profile writes replace the store atomically. Keep the cache
|
|
67
|
+
// local to this shell instance and recheck on the next frame after panel edits.
|
|
68
|
+
export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env): () => string | undefined {
|
|
69
|
+
const path = profilesFilePath(env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"));
|
|
70
|
+
let fingerprint: string | undefined;
|
|
71
|
+
let name: string | undefined;
|
|
72
|
+
return () => {
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(path, { bigint: true });
|
|
75
|
+
const next = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
76
|
+
if (next !== fingerprint) {
|
|
77
|
+
const result = readProfilesFileResult(path);
|
|
78
|
+
name = result.status === "valid" ? result.file.active : undefined;
|
|
79
|
+
fingerprint = next;
|
|
80
|
+
}
|
|
81
|
+
return name;
|
|
82
|
+
} catch {
|
|
83
|
+
fingerprint = undefined;
|
|
84
|
+
name = undefined;
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
function ambientDevBinary(): DevBinaryNotice | undefined {
|
|
58
91
|
try {
|
|
59
92
|
const override = resolveGentleAiDevBinaryOverride();
|
|
@@ -64,7 +97,7 @@ function ambientDevBinary(): DevBinaryNotice | undefined {
|
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
const defaultShellDeps: ShellDeps = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: shellGitRunner };
|
|
100
|
+
const defaultShellDeps: Omit<ShellDeps, "activeProfile"> = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: shellGitRunner };
|
|
68
101
|
|
|
69
102
|
interface AssistantUsageEntry {
|
|
70
103
|
type: string;
|
|
@@ -104,6 +137,7 @@ export function buildShellBarModel(
|
|
|
104
137
|
.map(([, text]) => text);
|
|
105
138
|
return {
|
|
106
139
|
cwd: shortenHome(ctx.sessionManager.getCwd(), home),
|
|
140
|
+
profile: options.profile,
|
|
107
141
|
branch: footerData.getGitBranch(),
|
|
108
142
|
dirty: options.dirty,
|
|
109
143
|
sessionName: ctx.sessionManager.getSessionName(),
|
|
@@ -151,8 +185,6 @@ interface PromptEditorDeps {
|
|
|
151
185
|
|
|
152
186
|
const PROMPT_FRAME_ROLE = "border";
|
|
153
187
|
|
|
154
|
-
const PETAL_PULSE_MS = 160;
|
|
155
|
-
|
|
156
188
|
export class GentlePromptEditor extends CustomEditor {
|
|
157
189
|
private promptState: PromptState = PROMPT_STATE.IDLE;
|
|
158
190
|
private tick = 0;
|
|
@@ -171,7 +203,7 @@ export class GentlePromptEditor extends CustomEditor {
|
|
|
171
203
|
this.pulse = setInterval(() => {
|
|
172
204
|
this.tick += 1;
|
|
173
205
|
this.deps.requestRender();
|
|
174
|
-
},
|
|
206
|
+
}, SHELL_PULSE_MS);
|
|
175
207
|
this.pulse.unref();
|
|
176
208
|
}
|
|
177
209
|
this.deps.requestRender();
|
|
@@ -203,9 +235,14 @@ export class GentlePromptEditor extends CustomEditor {
|
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
238
|
+
// Stable across extension module reloads; never infer ownership from a name.
|
|
239
|
+
const PROMPT_OWNER = Symbol.for("gentle-pi.prompt-owner");
|
|
240
|
+
type PromptFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>> & { [PROMPT_OWNER]?: boolean };
|
|
241
|
+
|
|
242
|
+
function installPrompt(ctx: ExtensionContext, onCreated: (prompt: GentlePromptEditor) => void): boolean {
|
|
243
|
+
const previous = ctx.ui.getEditorComponent() as PromptFactory | undefined;
|
|
244
|
+
if (previous && !previous[PROMPT_OWNER]) return false;
|
|
245
|
+
const factory: PromptFactory = (tui, theme, keybindings) => {
|
|
209
246
|
const prompt = new GentlePromptEditor(tui, theme, keybindings, {
|
|
210
247
|
fg: (color, text) => ctx.ui.theme.fg(color as Parameters<typeof ctx.ui.theme.fg>[0], text),
|
|
211
248
|
bold: (text) => ctx.ui.theme.bold(text),
|
|
@@ -214,15 +251,18 @@ function installPrompt(ctx: ExtensionContext, onCreated: (prompt: GentlePromptEd
|
|
|
214
251
|
});
|
|
215
252
|
onCreated(prompt);
|
|
216
253
|
return prompt;
|
|
217
|
-
}
|
|
254
|
+
};
|
|
255
|
+
factory[PROMPT_OWNER] = true;
|
|
256
|
+
ctx.ui.setEditorComponent(factory);
|
|
257
|
+
return true;
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
const CHANGES_WIDGET_KEY = "gentle-shell-changes";
|
|
221
261
|
const CHANGES_COMMAND_NAME = "gentle:changes";
|
|
222
262
|
const CHANGES_SHORTCUT_DEFAULT = "alt+g";
|
|
223
263
|
const CHANGES_POLL_DEFAULT_MS = 2000;
|
|
224
|
-
const CHANGES_WATCH_DEFAULT_MS = 5000;
|
|
225
264
|
const GIT_TIMEOUT_MS = 5000;
|
|
265
|
+
const COMMANDS_COMMAND_NAME = "gentle:commands";
|
|
226
266
|
const OVERLAY_HEIGHT_RATIO = 0.8;
|
|
227
267
|
const OVERLAY_MIN_ROWS = 8;
|
|
228
268
|
|
|
@@ -246,14 +286,6 @@ export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env
|
|
|
246
286
|
});
|
|
247
287
|
}
|
|
248
288
|
|
|
249
|
-
function lineCounter(cwd: string): LineCounter {
|
|
250
|
-
return async (path: string) => {
|
|
251
|
-
const text = await readFile(join(cwd, path), "utf8");
|
|
252
|
-
if (text.length === 0) return 0;
|
|
253
|
-
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
289
|
export async function loadFileDiff(git: GitRunner, file: ChangedFile): Promise<string> {
|
|
258
290
|
const args = file.status === CHANGE_STATUS.UNTRACKED ? ["diff", "--no-index", "--", "/dev/null", file.path] : ["diff", "HEAD", "--", file.path];
|
|
259
291
|
const result = await git(args);
|
|
@@ -295,28 +327,19 @@ function changesPollMs(env: NodeJS.ProcessEnv): number {
|
|
|
295
327
|
return positiveMs(env.GENTLE_PI_SHELL_CHANGES_POLL_MS, CHANGES_POLL_DEFAULT_MS);
|
|
296
328
|
}
|
|
297
329
|
|
|
298
|
-
// Background watch: the widget and the bar follow edits made outside pi.
|
|
299
|
-
// 0 or "off" disables it; tool events still refresh.
|
|
300
|
-
function changesWatchMs(env: NodeJS.ProcessEnv): number | undefined {
|
|
301
|
-
const value = env.GENTLE_PI_SHELL_CHANGES_WATCH_MS?.trim().toLowerCase();
|
|
302
|
-
if (value === "0" || value === "off") return undefined;
|
|
303
|
-
return positiveMs(value, CHANGES_WATCH_DEFAULT_MS);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
330
|
function changesFingerprint(model: ChangesModel): string {
|
|
307
|
-
return model.files.map((file) => `${file.path}:${file.status}:${file.added}:${file.deleted}`).join("|");
|
|
331
|
+
return [model.notice ?? "", ...model.files.map((file) => `${file.path}:${file.status}:${file.added}:${file.deleted}:${file.diffRevision ?? ""}:${file.countsUnavailable ?? ""}`)].join("|");
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
interface OverlayDeps {
|
|
311
|
-
|
|
335
|
+
loadDiff(root: string, file: ChangedFile): string;
|
|
312
336
|
worktrees(): WorktreeChanges[];
|
|
313
337
|
refresh(): Promise<ChangesModel>;
|
|
314
338
|
apply(ctx: ExtensionContext, model: ChangesModel): void;
|
|
315
339
|
pollMs: number;
|
|
316
340
|
}
|
|
317
341
|
|
|
318
|
-
//
|
|
319
|
-
// another agent, a git checkout) show up without reopening it.
|
|
342
|
+
// Refresh only the captured session model. Never read live files or Git here.
|
|
320
343
|
async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
|
|
321
344
|
let host: ExternalEditorHost | undefined;
|
|
322
345
|
let view: WorktreeChangesView | undefined;
|
|
@@ -334,7 +357,7 @@ async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Pro
|
|
|
334
357
|
view = new WorktreeChangesView(deps.worktrees(), {
|
|
335
358
|
theme,
|
|
336
359
|
rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
|
|
337
|
-
loadDiff: (root, file) =>
|
|
360
|
+
loadDiff: (root, file) => Promise.resolve(deps.loadDiff(root, file)),
|
|
338
361
|
onOpen: (root, file) => done({ root, file }),
|
|
339
362
|
onRefresh: () => void refresh(),
|
|
340
363
|
onClose: () => done(null),
|
|
@@ -353,6 +376,29 @@ async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Pro
|
|
|
353
376
|
}
|
|
354
377
|
}
|
|
355
378
|
|
|
379
|
+
// The command palette is a curated, grouped menu (Configuration, Session,
|
|
380
|
+
// Diagnostics, SDD, Skills), not a raw listing of every registered
|
|
381
|
+
// extension command: buildCommandPaletteGroups keeps only the catalog
|
|
382
|
+
// entries that are actually registered, so a missing extension never shows
|
|
383
|
+
// a dead row. Selecting an entry runs it exactly as if the user had typed
|
|
384
|
+
// the underlying slash command.
|
|
385
|
+
async function showCommandPalette(pi: ExtensionAPI, ctx: ExtensionContext, env: NodeJS.ProcessEnv): Promise<void> {
|
|
386
|
+
if (!ctx.hasUI) return;
|
|
387
|
+
const groups = buildCommandPaletteGroups(pi.getCommands(), {
|
|
388
|
+
"gentle:changes": changesShortcut(env),
|
|
389
|
+
"gentle:agents": agentsViewKey(env),
|
|
390
|
+
});
|
|
391
|
+
if (groups.length === 0) {
|
|
392
|
+
ctx.ui.notify("No Gentle commands are registered.", "info");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const result = await ctx.ui.custom<CommandPaletteResult>(
|
|
396
|
+
(tui, theme, _keybindings, done) => new CommandPalette(groups, done, theme, () => Math.max(0, tui.terminal.rows)),
|
|
397
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: "70%", minWidth: 60, maxHeight: "85%" } },
|
|
398
|
+
);
|
|
399
|
+
if (result?.type === "run") pi.sendUserMessage(`/${result.name}`, { expandPromptTemplates: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
356
402
|
function showChanges(ctx: ExtensionContext, model: ChangesModel): void {
|
|
357
403
|
if (model.files.length === 0) {
|
|
358
404
|
ctx.ui.setWidget(CHANGES_WIDGET_KEY, undefined);
|
|
@@ -365,20 +411,6 @@ function showChanges(ctx: ExtensionContext, model: ChangesModel): void {
|
|
|
365
411
|
return renderChangesWidget(model, theme, width);
|
|
366
412
|
},
|
|
367
413
|
invalidate() {},
|
|
368
|
-
}, {
|
|
369
|
-
render(width: number) {
|
|
370
|
-
const noun = model.files.length === 1 ? "file" : "files";
|
|
371
|
-
return renderCard({
|
|
372
|
-
title: "Changes",
|
|
373
|
-
tone: CARD_TONE.INFO,
|
|
374
|
-
body: [
|
|
375
|
-
`${model.files.length} ${noun} · ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}`,
|
|
376
|
-
"",
|
|
377
|
-
theme.fg("muted", `/${CHANGES_COMMAND_NAME}`),
|
|
378
|
-
],
|
|
379
|
-
}, theme, width, { expanded: true });
|
|
380
|
-
},
|
|
381
|
-
invalidate() {},
|
|
382
414
|
}),
|
|
383
415
|
{ placement: "belowEditor" },
|
|
384
416
|
);
|
|
@@ -451,8 +483,9 @@ export async function fetchCodexUsage(token: string | undefined, fetchFn: typeof
|
|
|
451
483
|
}
|
|
452
484
|
|
|
453
485
|
export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<ShellDeps> = {}): void {
|
|
486
|
+
installSessionChangeCapture(pi, env, overrides.resolveWorktree ?? resolveSessionWorktree);
|
|
454
487
|
if (!shellEnabled(env)) return;
|
|
455
|
-
const deps: ShellDeps = { ...defaultShellDeps, ...overrides };
|
|
488
|
+
const deps: ShellDeps = { ...defaultShellDeps, activeProfile: createActiveProfileReader(env), ...overrides };
|
|
456
489
|
const usage = new UsageStore();
|
|
457
490
|
let renderHost: ShellRenderHost | undefined;
|
|
458
491
|
let usageFetchedAt = 0;
|
|
@@ -500,11 +533,9 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
500
533
|
},
|
|
501
534
|
});
|
|
502
535
|
let prompt: GentlePromptEditor | undefined;
|
|
503
|
-
let changes:
|
|
536
|
+
let changes: SessionChanges | undefined;
|
|
504
537
|
let registry: SessionWorktreeRegistry | undefined;
|
|
505
538
|
let currentContext: ExtensionContext | undefined;
|
|
506
|
-
const pendingTools = new Map<string, { sessionId: string; path?: string }>();
|
|
507
|
-
let watch: NodeJS.Timeout | undefined;
|
|
508
539
|
let shown = "";
|
|
509
540
|
const applyChanges = (ctx: ExtensionContext, model: ChangesModel) => {
|
|
510
541
|
const fingerprint = changesFingerprint(model);
|
|
@@ -515,22 +546,21 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
515
546
|
const refreshChanges = async (ctx: ExtensionContext) => {
|
|
516
547
|
const tracker = changes;
|
|
517
548
|
if (!tracker || !ctx.hasUI || registry?.sessionId !== ctx.sessionManager.getSessionId()) return;
|
|
549
|
+
tracker.restore(ctx.sessionManager.getEntries());
|
|
518
550
|
const model = await tracker.refresh();
|
|
519
551
|
if (changes === tracker) applyChanges(ctx, model);
|
|
520
552
|
};
|
|
521
|
-
const
|
|
522
|
-
if (watch) clearInterval(watch);
|
|
523
|
-
watch = undefined;
|
|
524
|
-
};
|
|
525
|
-
const unsubscribeWorktrees = pi.events.on(SESSION_WORKTREE_CHANGED, (data) => {
|
|
553
|
+
const unsubscribeWorktrees = pi.events.on(SESSION_CHANGE_EVENT, (data) => {
|
|
526
554
|
if (!currentContext || !registry || (data as { sessionId?: string } | undefined)?.sessionId !== registry.sessionId) return;
|
|
555
|
+
const notice = (data as { notice?: string } | undefined)?.notice;
|
|
556
|
+
if (notice) { if (changes) changes.notice = notice; currentContext.ui.notify(notice, "warning"); }
|
|
527
557
|
void refreshChanges(currentContext);
|
|
528
558
|
});
|
|
529
559
|
pi.registerTool({
|
|
530
560
|
name: "session_worktree_register",
|
|
531
561
|
renderShell: "self",
|
|
532
562
|
label: "Register session worktree",
|
|
533
|
-
description: "Register a worktree
|
|
563
|
+
description: "Register a worktree in the same Git clone for session coordination. Registration does not attribute file changes; Changes shows captured write/edit operations only.",
|
|
534
564
|
parameters: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Worktree path to include in this session." } } } as never,
|
|
535
565
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
536
566
|
if (!registry || registry.sessionId !== ctx.sessionManager.getSessionId()) throw new Error("No active session worktree registry.");
|
|
@@ -540,16 +570,13 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
540
570
|
},
|
|
541
571
|
});
|
|
542
572
|
pi.on("session_start", async (_event, ctx) => {
|
|
543
|
-
stopWatch();
|
|
544
573
|
registry?.close();
|
|
545
|
-
pendingTools.clear();
|
|
546
574
|
currentContext = ctx;
|
|
547
575
|
changes = undefined;
|
|
548
576
|
registry = new SessionWorktreeRegistry(pi, ctx.sessionManager, ctx.cwd, deps.resolveWorktree);
|
|
549
577
|
registry.start();
|
|
550
|
-
const sessionRegistry = registry;
|
|
551
578
|
if (!ctx.hasUI) return;
|
|
552
|
-
changes = new
|
|
579
|
+
changes = new SessionChanges(ctx.sessionManager.getSessionId(), ctx.sessionManager.getEntries());
|
|
553
580
|
const tracker = changes;
|
|
554
581
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
555
582
|
renderHost = { requestRender: () => tui.requestRender(), invalidateSidebar: () => invalidateSidebar(tui) };
|
|
@@ -558,7 +585,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
558
585
|
// part for: model, effort, context, cost, session name and extension
|
|
559
586
|
// statuses. The digest is what keeps the fullscreen memo honest, and it
|
|
560
587
|
// rebuilds the model exactly as the narrow bottom bar does every frame.
|
|
561
|
-
const footerModel = () =>
|
|
588
|
+
const footerModel = (): ShellBarModel => ({
|
|
589
|
+
...buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? ""), profile: deps.activeProfile() }),
|
|
590
|
+
changes: { files: tracker.model.files.length, added: tracker.model.added, deleted: tracker.model.deleted, notice: tracker.model.notice },
|
|
591
|
+
});
|
|
562
592
|
const part = sidebarPart(tui, "footer", bottom, {
|
|
563
593
|
digest: () => JSON.stringify(footerModel()),
|
|
564
594
|
render: (width) => renderShellSidebarBar(footerModel(), theme, width),
|
|
@@ -568,11 +598,13 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
568
598
|
return { ...part, dispose() { uninstall(); part.dispose(); } };
|
|
569
599
|
});
|
|
570
600
|
void refreshUsage(ctx, true);
|
|
571
|
-
installPrompt(ctx, (created) => {
|
|
601
|
+
const ownsPrompt = installPrompt(ctx, (created) => {
|
|
602
|
+
prompt?.dispose();
|
|
572
603
|
prompt = created;
|
|
573
604
|
});
|
|
574
|
-
//
|
|
575
|
-
|
|
605
|
+
// Hide native feedback only when our petal replaces it. Native transcript
|
|
606
|
+
// thinking blocks remain Pi-owned; this changes only the supported loader UI.
|
|
607
|
+
if (ownsPrompt) ctx.ui.setWorkingVisible(false);
|
|
576
608
|
const notice = deps.devBinary();
|
|
577
609
|
ctx.ui.setWidget(
|
|
578
610
|
DEV_BINARY_WIDGET_KEY,
|
|
@@ -580,24 +612,21 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
580
612
|
? (_tui, theme) => spaced(cardComponent(devBinaryCard(notice), theme, { expanded: true }))
|
|
581
613
|
: undefined,
|
|
582
614
|
);
|
|
583
|
-
await tracker.start();
|
|
584
615
|
if (changes !== tracker) return;
|
|
585
616
|
shown = "";
|
|
586
617
|
applyChanges(ctx, tracker.model);
|
|
587
|
-
stopWatch();
|
|
588
|
-
const watchMs = changesWatchMs(env);
|
|
589
|
-
if (watchMs) {
|
|
590
|
-
watch = setInterval(() => void refreshChanges(ctx), watchMs);
|
|
591
|
-
watch.unref();
|
|
592
|
-
}
|
|
593
618
|
});
|
|
594
|
-
pi.on("session_shutdown", () => {
|
|
595
|
-
|
|
619
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
620
|
+
prompt?.dispose();
|
|
621
|
+
prompt = undefined;
|
|
622
|
+
if ((ctx.ui.getEditorComponent() as PromptFactory | undefined)?.[PROMPT_OWNER]) {
|
|
623
|
+
ctx.ui.setEditorComponent(undefined);
|
|
624
|
+
ctx.ui.setWorkingVisible(true);
|
|
625
|
+
}
|
|
596
626
|
registry?.close();
|
|
597
627
|
registry = undefined;
|
|
598
628
|
changes = undefined;
|
|
599
629
|
currentContext = undefined;
|
|
600
|
-
pendingTools.clear();
|
|
601
630
|
unsubscribeWorktrees();
|
|
602
631
|
});
|
|
603
632
|
const openChanges = async (ctx: ExtensionContext) => {
|
|
@@ -605,46 +634,43 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
605
634
|
const tracker = changes;
|
|
606
635
|
const model = await tracker.refresh();
|
|
607
636
|
if (model.files.length === 0) {
|
|
608
|
-
ctx.ui.notify("No changes
|
|
637
|
+
ctx.ui.notify("No captured agent changes. Only successful write/edit operations from this session and its subagents are shown; shell changes are not attributed.", "info");
|
|
609
638
|
return;
|
|
610
639
|
}
|
|
611
|
-
await showChangesOverlay(ctx, {
|
|
640
|
+
await showChangesOverlay(ctx, { loadDiff: (root, file) => tracker.loadDiff(root, file), worktrees: () => tracker.worktrees, refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env) });
|
|
612
641
|
};
|
|
613
642
|
pi.registerCommand(CHANGES_COMMAND_NAME, {
|
|
614
|
-
description: "Browse this session
|
|
643
|
+
description: "Browse captured write/edit changes from this agent session and its subagents, excluding preexisting and external edits. Shell changes are not attributed. Press o to open $EDITOR.",
|
|
615
644
|
handler: async (_args, ctx) => openChanges(ctx),
|
|
616
645
|
});
|
|
617
646
|
const shortcut = changesShortcut(env);
|
|
618
647
|
if (shortcut) {
|
|
619
648
|
pi.registerShortcut(shortcut as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
620
|
-
description: "Open
|
|
649
|
+
description: "Open captured agent session changes",
|
|
621
650
|
handler: async (ctx) => openChanges(ctx),
|
|
622
651
|
});
|
|
623
652
|
}
|
|
653
|
+
pi.registerCommand(COMMANDS_COMMAND_NAME, {
|
|
654
|
+
description: "Open the command palette: a curated, grouped menu of Gentle commands.",
|
|
655
|
+
handler: async (_args, ctx) => showCommandPalette(pi, ctx, env),
|
|
656
|
+
});
|
|
657
|
+
const commandsShortcut = commandsKey(env);
|
|
658
|
+
if (commandsShortcut) {
|
|
659
|
+
pi.registerShortcut(commandsShortcut as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
660
|
+
description: "Open the command palette",
|
|
661
|
+
handler: async (ctx) => showCommandPalette(pi, ctx, env),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
624
664
|
pi.on("agent_start", (_event, ctx) => {
|
|
625
665
|
prompt?.setWorking(true);
|
|
626
666
|
// The dev-binary card is a startup notice: it leaves with the first prompt.
|
|
627
667
|
if (ctx.hasUI) ctx.ui.setWidget(DEV_BINARY_WIDGET_KEY, undefined);
|
|
628
668
|
});
|
|
629
|
-
pi.on("
|
|
669
|
+
pi.on("agent_settled", () => {
|
|
630
670
|
prompt?.setWorking(false);
|
|
631
|
-
await refreshChanges(ctx);
|
|
632
|
-
void refreshUsage(ctx, false);
|
|
633
|
-
});
|
|
634
|
-
pi.on("tool_execution_start", (event, ctx) => {
|
|
635
|
-
pendingTools.set(event.toolCallId, { sessionId: ctx.sessionManager.getSessionId() });
|
|
636
671
|
});
|
|
637
|
-
pi.on("
|
|
638
|
-
const pending = pendingTools.get(event.toolCallId);
|
|
639
|
-
if (pending?.sessionId === ctx.sessionManager.getSessionId()) pending.path = toolWorktreePath(event.toolName, event.input);
|
|
640
|
-
});
|
|
641
|
-
pi.on("tool_execution_end", async (event, ctx) => {
|
|
642
|
-
const pending = pendingTools.get(event.toolCallId);
|
|
643
|
-
pendingTools.delete(event.toolCallId);
|
|
644
|
-
if (!event.isError && pending?.path !== undefined && pending.sessionId === registry?.sessionId && pending.sessionId === ctx.sessionManager.getSessionId()) {
|
|
645
|
-
try { registry.register(pending.path, `tool:${event.toolName}`); }
|
|
646
|
-
catch { /* Non-project paths and missing roots do not expand the registry. */ }
|
|
647
|
-
}
|
|
672
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
648
673
|
await refreshChanges(ctx);
|
|
674
|
+
void refreshUsage(ctx, false);
|
|
649
675
|
});
|
|
650
676
|
}
|
package/extensions/pi-pretty.ts
CHANGED
|
@@ -1,29 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { shellEnabled } from "../lib/shell-bar.ts";
|
|
4
3
|
import {
|
|
5
4
|
mergeDisabledTools,
|
|
6
5
|
PI_PRETTY_SUPPRESSED_TOOL_NAMES,
|
|
7
6
|
quietToolsEnabled,
|
|
8
7
|
} from "../lib/quiet-tools-config.ts";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
fileURLToPath(new URL("../package.json", import.meta.url)),
|
|
12
|
-
);
|
|
13
|
-
const requireFromRealPackage = createRequire(packageJsonPath);
|
|
14
|
-
const piPrettyModule = requireFromRealPackage("@heyhuynhgiabuu/pi-pretty");
|
|
9
|
+
type PiPrettyExtension = (pi: unknown, deps?: unknown) => unknown;
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
typeof piPrettyModule === "function"
|
|
18
|
-
? piPrettyModule
|
|
19
|
-
: piPrettyModule.default;
|
|
11
|
+
let piPrettyExtensionPromise: Promise<PiPrettyExtension> | undefined;
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
async function loadPiPrettyExtension(): Promise<PiPrettyExtension> {
|
|
14
|
+
return piPrettyExtensionPromise ??= import("@heyhuynhgiabuu/pi-pretty").then(
|
|
15
|
+
(piPrettyModule) => {
|
|
16
|
+
const moduleCandidate: unknown = piPrettyModule;
|
|
17
|
+
const extension =
|
|
18
|
+
typeof moduleCandidate === "function"
|
|
19
|
+
? moduleCandidate
|
|
20
|
+
: piPrettyModule.default;
|
|
21
|
+
if (typeof extension !== "function") {
|
|
22
|
+
throw new TypeError("pi-pretty must export an extension function");
|
|
23
|
+
}
|
|
24
|
+
return extension as PiPrettyExtension;
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function gentlePiPrettyExtension(
|
|
30
|
+
pi: unknown,
|
|
31
|
+
deps?: unknown,
|
|
32
|
+
bundled?: PiPrettyExtension,
|
|
33
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
34
|
+
): Promise<unknown> {
|
|
22
35
|
if (quietToolsEnabled()) {
|
|
23
36
|
process.env.PRETTY_DISABLE_TOOLS = mergeDisabledTools(
|
|
24
37
|
process.env.PRETTY_DISABLE_TOOLS,
|
|
25
38
|
PI_PRETTY_SUPPRESSED_TOOL_NAMES,
|
|
26
39
|
);
|
|
27
40
|
}
|
|
28
|
-
|
|
41
|
+
const extension = bundled ?? await loadPiPrettyExtension();
|
|
42
|
+
if (!shellEnabled(env)) return extension(pi, deps);
|
|
43
|
+
const api = pi as ExtensionAPI;
|
|
44
|
+
// Scope interception to this bundled dependency, never to the shared host UI.
|
|
45
|
+
// Its tools/autocomplete/status APIs pass through; prompt ownership stays with
|
|
46
|
+
// Gentle Shell (or a genuine third-party editor). This also blocks stale restores.
|
|
47
|
+
const owned = new Set(["setEditorComponent", "setWorkingIndicator", "setWorkingMessage", "setWorkingVisible"]);
|
|
48
|
+
const delegated = new Proxy(api, {
|
|
49
|
+
get(target, key) {
|
|
50
|
+
if (key !== "on") return Reflect.get(target, key);
|
|
51
|
+
return (event: Parameters<ExtensionAPI["on"]>[0], handler: (event: unknown, ctx: ExtensionContext) => unknown) => {
|
|
52
|
+
api.on(event, ((event: unknown, ctx: ExtensionContext) => {
|
|
53
|
+
if (ctx.mode !== "tui") return handler(event, ctx);
|
|
54
|
+
const ui = new Proxy(ctx.ui, {
|
|
55
|
+
get(target, key) {
|
|
56
|
+
return owned.has(String(key)) ? () => {} : Reflect.get(target, key);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return handler(event, new Proxy(ctx, { get: (target, key) => key === "ui" ? ui : Reflect.get(target, key) }));
|
|
60
|
+
}) as never);
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const result = await extension(delegated, deps);
|
|
65
|
+
// GentlePromptEditor already carries the live working state in its frame.
|
|
66
|
+
// Hide Pi's separate loader row to avoid repeating Thinking above the input;
|
|
67
|
+
// transcript thinking blocks remain untouched as historical reasoning markers.
|
|
68
|
+
api.on("session_start", (_event, ctx) => {
|
|
69
|
+
if (ctx.mode === "tui") ctx.ui.setWorkingVisible(false);
|
|
70
|
+
});
|
|
71
|
+
api.on("session_shutdown", (_event, ctx) => {
|
|
72
|
+
if (ctx.mode !== "tui") return;
|
|
73
|
+
ctx.ui.setWorkingVisible(true);
|
|
74
|
+
ctx.ui.setWorkingIndicator();
|
|
75
|
+
ctx.ui.setWorkingMessage();
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
29
78
|
}
|
|
@@ -238,7 +238,7 @@ function shellTokens(command: string): ShellTokenization {
|
|
|
238
238
|
function isAssignment(token: string): boolean {
|
|
239
239
|
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
240
240
|
}
|
|
241
|
-
const SDD_ATTEMPT_VERBS = new Set(["
|
|
241
|
+
const SDD_ATTEMPT_VERBS = new Set(["grant"]);
|
|
242
242
|
|
|
243
243
|
const REVIEW_DIRECT_OPERATIONS = new Set([
|
|
244
244
|
"capabilities",
|
|
@@ -354,7 +354,6 @@ function gentleAiOperationPathFrom(tokens: string[]): string {
|
|
|
354
354
|
if (tokens[0] === "sdd-attempt") {
|
|
355
355
|
const verb = tokens[1] ?? "";
|
|
356
356
|
if (!SDD_ATTEMPT_VERBS.has(verb)) return "sdd attempt";
|
|
357
|
-
if (verb !== "grant") return `sdd attempt ${verb}`;
|
|
358
357
|
const rootCount = authorizationRootCount(tokens);
|
|
359
358
|
return rootCount > 0
|
|
360
359
|
? `sdd attempt grant · ${rootCount} root${rootCount === 1 ? "" : "s"}`
|
|
@@ -29,14 +29,14 @@ const BANNER_PALETTES: Record<BannerColor, { rose: [number, number, number]; lab
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const TEXT_LOGO = [
|
|
32
|
-
" ▄▄▄▀▀▀▀▀██ ▄▄▀▄▄
|
|
33
|
-
" ▄▄█▀▀▒▒▒▒▒▄▄█▀▒ ▄██ ▄▄█▀█▄█▀▒▒
|
|
34
|
-
" ▄▄██▀▒▒▒▒▒▄▄▄▀▀▒▒▒▒ ▄▄▄ ▀▀▀▀██▀▀▀▀▀███▀█▄▀▀▒▒▒▒
|
|
35
|
-
" ▄██▀▒▒▒▒ ▒▒▄▄█ ▄▄▄▀██ ▄▄▄▀▀▀▄ ▄██▀▒▒▒▒▄██▀▀▀▒▄▄███
|
|
36
|
-
" ██▀▒▒▒
|
|
37
|
-
" ▀█▄▄▄▄▄▀▀▀█▄▄███▄▒▀▀▀▀▀▀▒▀▀▒▒▀▀▀▀▒██▄▄▀▀▀ ▀█▄▀▀▀
|
|
38
|
-
" ▒▄▄▄█▀▀▀█▄█▀▀▒▒▒▒ ▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒
|
|
39
|
-
" ▄▄▀▀ ▒▒▒▒▄██▀▒▒▒▒
|
|
32
|
+
" ▄▄▄▀▀▀▀▀██ ▄▄▀▄▄ ▄▄█▀▀▀██ ▄▄▀██ ▄▄▀▄▄ ▄▄▀▄▄",
|
|
33
|
+
" ▄▄█▀▀▒▒▒▒▒▄▄█▀▒ ▄██ ▄▄█▀█▄█▀▒▒ ▄█▀▀▒▒▒▄█▀▒ ▄██▄█▀▒ ▄▄█▀█▄█▀▄▄█▀█▄█▀▒",
|
|
34
|
+
" ▄▄██▀▒▒▒▒▒▄▄▄▀▀▒▒▒▒ ▄▄▄ ▀▀▀▀██▀▀▀▀▀███▀█▄▀▀▒▒▒▒ ██▄▄▒▒▒▒▒▒▒ ▄██▀▀▒▒▒ ▄██▀█▄▀▀▄██▀█▄▀▀▒▒▒",
|
|
35
|
+
" ▄██▀▒▒▒▒ ▒▒▄▄█ ▄▄▄▀██ ▄▄▄▀▀▀▄ ▄██▀▒▒▒▒▄██▀▀▀▒▄▄███ ▀▀▀██▄▄ ▄██▀▒▄▄▀██ ▄▄▀▀██ ██▀▀▀▒▒▒██▀▀▀▒▒▒",
|
|
36
|
+
" ██▀▒▒▒ ▄▄▄███▀▄██▀▀▀▄▄██▀▀▄█▀▄▄██▀▒▒▒▄▄██▀▒▒▄██▀▀▀▄▄ ▄▄ ▀██▄▄██▀▄██▀██▒ ▄██▄▄▀▒ █▀▒▒▒ █▀▒▒▒",
|
|
37
|
+
" ▀█▄▄▄▄▄▀▀▀█▄▄███▄▒▀▀▀▀▀▀▒▀▀▒▒▀▀▀▀▒██▄▄▀▀▀ ▀█▄▀▀▀ ▀▀▀▀▀▒▒ ▀█▄▄▄▄█▀▀▒▒▀▀▒▒▀▀▒ ██▄▄▀▀█▄▄▄▄▀▀█▄▄▀▀▀▄▄▀█▄▄▀▀▀",
|
|
38
|
+
" ▒▄▄▄█▀▀▀█▄█▀▀▒▒▒▒ ▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▀▀▀▀▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒",
|
|
39
|
+
" ▄▄▀▀ ▒▒▒▒▄██▀▒▒▒▒",
|
|
40
40
|
" ▄█ ▒▒▒▄▄██▀▀▒▒▒▒",
|
|
41
41
|
" ▀▀▀▀▀▀▒▒▒▒▒▒",
|
|
42
42
|
" ▒▒▒▒▒▒",
|
|
@@ -183,7 +183,8 @@ function buildLetterSpans(bounds: Span, weights: number[]): Span[] {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
const LOGO_BOUNDS = computeLogoBounds(TEXT_LOGO);
|
|
186
|
-
|
|
186
|
+
// Variable-width script regions, including the gap before Shell and shadows.
|
|
187
|
+
const LETTER_WEIGHTS = [22, 9, 8, 9, 8, 9, 10, 13, 8, 8, 13]; // G E N T L E S H E L L
|
|
187
188
|
const LETTER_SPANS = buildLetterSpans(LOGO_BOUNDS, LETTER_WEIGHTS);
|
|
188
189
|
|
|
189
190
|
function letterIndexAtX(x: number): number {
|
package/lib/agent-home.ts
CHANGED
|
@@ -6,3 +6,11 @@ import { join } from "node:path";
|
|
|
6
6
|
export function resolveGentlePiAgentHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
7
7
|
return env.GENTLE_PI_AGENT_HOME || env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
// The Gentle AI config directory outside `~/.pi/agent`: everything the extension
|
|
11
|
+
// commands own lives here. It is defined once because the launch-time profile pin
|
|
12
|
+
// resolver reads the same profiles store the `/gentle:profiles` panel writes, and
|
|
13
|
+
// two spellings of the override would silently read two different stores.
|
|
14
|
+
export function gentlePiConfigHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
15
|
+
return env.GENTLE_PI_CONFIG_HOME || join(homedir(), ".pi", "gentle-ai");
|
|
16
|
+
}
|