gentle-pi 2.5.0 → 2.6.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 +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -1,17 +1,20 @@
|
|
|
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
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { renderShellBar, shellEnabled, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
|
|
8
|
-
import { CHANGE_STATUS,
|
|
9
|
-
import {
|
|
7
|
+
import { renderShellBar, renderShellSidebarBar, shellEnabled, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
|
|
8
|
+
import { CHANGE_STATUS, WorktreeChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
|
|
9
|
+
import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
|
|
10
|
+
import { SessionWorktreeRegistry, SESSION_WORKTREE_CHANGED, resolveSessionWorktree, toolWorktreePath, worktreeGitEnvironment, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
10
11
|
import { CARD_TONE, renderCard, type Card, type CardTheme } from "../lib/shell-card.ts";
|
|
11
12
|
import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
|
|
12
|
-
import { framePromptLines,
|
|
13
|
+
import { framePromptLines, PROMPT_HINT, PROMPT_STATE, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
|
|
13
14
|
import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, parseCodexUsage, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
|
|
14
15
|
import { UsageView } from "../lib/shell-usage-view.ts";
|
|
16
|
+
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
17
|
+
import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
15
18
|
|
|
16
19
|
// Gentle Shell: the visual layer gentle-pi puts on top of pi. It installs the
|
|
17
20
|
// status bar, the petal prompt, the working-tree changes widget and overlay,
|
|
@@ -26,6 +29,7 @@ export interface ShellFooterData {
|
|
|
26
29
|
|
|
27
30
|
interface ShellRenderHost {
|
|
28
31
|
requestRender(): void;
|
|
32
|
+
invalidateSidebar?(): void;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
interface ShellBarComponent {
|
|
@@ -46,6 +50,8 @@ export interface ShellDeps {
|
|
|
46
50
|
fetch: typeof fetch;
|
|
47
51
|
now(): number;
|
|
48
52
|
devBinary(): DevBinaryNotice | undefined;
|
|
53
|
+
resolveWorktree: WorktreeResolver;
|
|
54
|
+
gitRunner(cwd: string): GitRunner;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function ambientDevBinary(): DevBinaryNotice | undefined {
|
|
@@ -58,7 +64,7 @@ function ambientDevBinary(): DevBinaryNotice | undefined {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
const defaultShellDeps: ShellDeps = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary };
|
|
67
|
+
const defaultShellDeps: ShellDeps = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: shellGitRunner };
|
|
62
68
|
|
|
63
69
|
interface AssistantUsageEntry {
|
|
64
70
|
type: string;
|
|
@@ -121,7 +127,10 @@ export function createShellBarComponent(
|
|
|
121
127
|
dirty: () => number | undefined = () => undefined,
|
|
122
128
|
usage: () => ProviderUsage | undefined = () => undefined,
|
|
123
129
|
): ShellBarComponent {
|
|
124
|
-
const unsubscribe = footerData.onBranchChange(() =>
|
|
130
|
+
const unsubscribe = footerData.onBranchChange(() => {
|
|
131
|
+
host.invalidateSidebar?.();
|
|
132
|
+
host.requestRender();
|
|
133
|
+
});
|
|
125
134
|
return {
|
|
126
135
|
render(width: number) {
|
|
127
136
|
return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width);
|
|
@@ -136,7 +145,6 @@ export function createShellBarComponent(
|
|
|
136
145
|
interface PromptEditorDeps {
|
|
137
146
|
fg: (color: string, text: string) => string;
|
|
138
147
|
bold: (text: string) => string;
|
|
139
|
-
paint?: (line: string) => string;
|
|
140
148
|
requestRender(): void;
|
|
141
149
|
pending(): boolean;
|
|
142
150
|
}
|
|
@@ -181,7 +189,6 @@ export class GentlePromptEditor extends CustomEditor {
|
|
|
181
189
|
borderColor: (text) => this.deps.fg(PROMPT_FRAME_ROLE, text),
|
|
182
190
|
fg: this.deps.fg,
|
|
183
191
|
bold: this.deps.bold,
|
|
184
|
-
paint: this.deps.paint,
|
|
185
192
|
});
|
|
186
193
|
}
|
|
187
194
|
|
|
@@ -202,7 +209,6 @@ function installPrompt(ctx: ExtensionContext, onCreated: (prompt: GentlePromptEd
|
|
|
202
209
|
const prompt = new GentlePromptEditor(tui, theme, keybindings, {
|
|
203
210
|
fg: (color, text) => ctx.ui.theme.fg(color as Parameters<typeof ctx.ui.theme.fg>[0], text),
|
|
204
211
|
bold: (text) => ctx.ui.theme.bold(text),
|
|
205
|
-
paint: panelPainter(ctx.ui.theme.getBgAnsi("customMessageBg")),
|
|
206
212
|
requestRender: () => tui.requestRender(),
|
|
207
213
|
pending: () => ctx.hasPendingMessages(),
|
|
208
214
|
});
|
|
@@ -220,11 +226,24 @@ const GIT_TIMEOUT_MS = 5000;
|
|
|
220
226
|
const OVERLAY_HEIGHT_RATIO = 0.8;
|
|
221
227
|
const OVERLAY_MIN_ROWS = 8;
|
|
222
228
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile): GitRunner {
|
|
230
|
+
// Pi exec cannot replace the inherited environment. Use argv directly and
|
|
231
|
+
// a complete sanitized environment for discovery, status, and lazy diffs.
|
|
232
|
+
const childEnv = worktreeGitEnvironment(env);
|
|
233
|
+
return (args) => new Promise((resolve) => {
|
|
234
|
+
run("git", ["-C", cwd, ...args], {
|
|
235
|
+
env: childEnv,
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
shell: false,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
timeout: GIT_TIMEOUT_MS,
|
|
240
|
+
// Pi exec accumulates output without a maxBuffer cap. In particular,
|
|
241
|
+
// large porcelain inventories must not become partial successful scans.
|
|
242
|
+
maxBuffer: Infinity,
|
|
243
|
+
}, (error, stdout) => {
|
|
244
|
+
resolve({ stdout, code: error ? typeof error.code === "number" ? error.code : 1 : 0 });
|
|
245
|
+
});
|
|
246
|
+
});
|
|
228
247
|
}
|
|
229
248
|
|
|
230
249
|
function lineCounter(cwd: string): LineCounter {
|
|
@@ -247,13 +266,13 @@ export interface ExternalEditorHost {
|
|
|
247
266
|
requestRender(force?: boolean): void;
|
|
248
267
|
}
|
|
249
268
|
|
|
250
|
-
export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync): boolean {
|
|
269
|
+
export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync, cwd?: string): boolean {
|
|
251
270
|
const command = env.VISUAL || env.EDITOR;
|
|
252
271
|
if (!command) return false;
|
|
253
272
|
const [editor, ...editorArgs] = command.split(" ");
|
|
254
273
|
host.stop();
|
|
255
274
|
try {
|
|
256
|
-
spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
|
|
275
|
+
spawn(editor, [...editorArgs, path], { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
257
276
|
} finally {
|
|
258
277
|
host.start();
|
|
259
278
|
host.requestRender(true);
|
|
@@ -289,7 +308,8 @@ function changesFingerprint(model: ChangesModel): string {
|
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
interface OverlayDeps {
|
|
292
|
-
git: GitRunner;
|
|
311
|
+
git(root: string): GitRunner;
|
|
312
|
+
worktrees(): WorktreeChanges[];
|
|
293
313
|
refresh(): Promise<ChangesModel>;
|
|
294
314
|
apply(ctx: ExtensionContext, model: ChangesModel): void;
|
|
295
315
|
pollMs: number;
|
|
@@ -297,24 +317,26 @@ interface OverlayDeps {
|
|
|
297
317
|
|
|
298
318
|
// While the overlay is open, git is polled so edits made outside pi (nvim,
|
|
299
319
|
// another agent, a git checkout) show up without reopening it.
|
|
300
|
-
async function showChangesOverlay(ctx: ExtensionContext,
|
|
320
|
+
async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
|
|
301
321
|
let host: ExternalEditorHost | undefined;
|
|
302
|
-
let view:
|
|
303
|
-
const
|
|
322
|
+
let view: WorktreeChangesView | undefined;
|
|
323
|
+
const refresh = async () => {
|
|
304
324
|
const latest = await deps.refresh();
|
|
305
|
-
view?.update(
|
|
325
|
+
view?.update(deps.worktrees());
|
|
306
326
|
deps.apply(ctx, latest);
|
|
307
|
-
}
|
|
327
|
+
};
|
|
328
|
+
const poll = setInterval(() => void refresh(), deps.pollMs);
|
|
308
329
|
poll.unref();
|
|
309
330
|
try {
|
|
310
|
-
const chosen = await ctx.ui.custom<ChangedFile | null>(
|
|
331
|
+
const chosen = await ctx.ui.custom<{ root: string; file: ChangedFile } | null>(
|
|
311
332
|
(tui, theme, _keybindings, done) => {
|
|
312
333
|
host = tui;
|
|
313
|
-
view = new
|
|
334
|
+
view = new WorktreeChangesView(deps.worktrees(), {
|
|
314
335
|
theme,
|
|
315
|
-
rows: Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
|
|
316
|
-
loadDiff: (file) => loadFileDiff(deps.git, file),
|
|
317
|
-
onOpen: (file) => done(file),
|
|
336
|
+
rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
|
|
337
|
+
loadDiff: (root, file) => loadFileDiff(deps.git(root), file),
|
|
338
|
+
onOpen: (root, file) => done({ root, file }),
|
|
339
|
+
onRefresh: () => void refresh(),
|
|
318
340
|
onClose: () => done(null),
|
|
319
341
|
requestRender: () => tui.requestRender(),
|
|
320
342
|
});
|
|
@@ -323,9 +345,11 @@ async function showChangesOverlay(ctx: ExtensionContext, model: ChangesModel, de
|
|
|
323
345
|
{ overlay: true, overlayOptions: { width: "92%", anchor: "center" } },
|
|
324
346
|
);
|
|
325
347
|
if (!chosen || !host) return;
|
|
326
|
-
if (!openInExternalEditor(host, chosen.path)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
|
|
348
|
+
if (!openInExternalEditor(host, chosen.file.path, process.env, spawnSync, chosen.root)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
|
|
327
349
|
} finally {
|
|
328
350
|
clearInterval(poll);
|
|
351
|
+
view?.dispose();
|
|
352
|
+
view = undefined;
|
|
329
353
|
}
|
|
330
354
|
}
|
|
331
355
|
|
|
@@ -336,11 +360,25 @@ function showChanges(ctx: ExtensionContext, model: ChangesModel): void {
|
|
|
336
360
|
}
|
|
337
361
|
ctx.ui.setWidget(
|
|
338
362
|
CHANGES_WIDGET_KEY,
|
|
339
|
-
(
|
|
363
|
+
(tui, theme) => sidebarPart(tui, "changes", {
|
|
340
364
|
render(width: number) {
|
|
341
365
|
return renderChangesWidget(model, theme, width);
|
|
342
366
|
},
|
|
343
367
|
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() {},
|
|
344
382
|
}),
|
|
345
383
|
{ placement: "belowEditor" },
|
|
346
384
|
);
|
|
@@ -359,7 +397,6 @@ function messageText(content: string | Array<{ type: string; text?: string }>):
|
|
|
359
397
|
interface CardComponentOptions {
|
|
360
398
|
expanded: boolean;
|
|
361
399
|
hint?: string;
|
|
362
|
-
paint?: (line: string) => string;
|
|
363
400
|
}
|
|
364
401
|
|
|
365
402
|
function cardComponent(card: Card, theme: CardTheme, options: CardComponentOptions) {
|
|
@@ -429,12 +466,14 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
429
466
|
const fetched = await fetchCodexUsage(token, deps.fetch, deps.now());
|
|
430
467
|
if (!fetched) return;
|
|
431
468
|
usage.record(fetched);
|
|
469
|
+
renderHost?.invalidateSidebar?.();
|
|
432
470
|
renderHost?.requestRender();
|
|
433
471
|
};
|
|
434
472
|
pi.on("after_provider_response", (event) => {
|
|
435
473
|
const parsed = parseUsageHeaders(event.headers, deps.now());
|
|
436
474
|
if (!parsed) return;
|
|
437
475
|
usage.record(parsed);
|
|
476
|
+
renderHost?.invalidateSidebar?.();
|
|
438
477
|
renderHost?.requestRender();
|
|
439
478
|
});
|
|
440
479
|
pi.registerMessageRenderer(REVIEW_PREFLIGHT_TYPE, (message, options, theme) => {
|
|
@@ -461,7 +500,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
461
500
|
},
|
|
462
501
|
});
|
|
463
502
|
let prompt: GentlePromptEditor | undefined;
|
|
464
|
-
let changes:
|
|
503
|
+
let changes: WorktreeChangesTracker | undefined;
|
|
504
|
+
let registry: SessionWorktreeRegistry | undefined;
|
|
505
|
+
let currentContext: ExtensionContext | undefined;
|
|
506
|
+
const pendingTools = new Map<string, { sessionId: string; path?: string }>();
|
|
465
507
|
let watch: NodeJS.Timeout | undefined;
|
|
466
508
|
let shown = "";
|
|
467
509
|
const applyChanges = (ctx: ExtensionContext, model: ChangesModel) => {
|
|
@@ -471,20 +513,59 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
471
513
|
showChanges(ctx, model);
|
|
472
514
|
};
|
|
473
515
|
const refreshChanges = async (ctx: ExtensionContext) => {
|
|
474
|
-
|
|
475
|
-
|
|
516
|
+
const tracker = changes;
|
|
517
|
+
if (!tracker || !ctx.hasUI || registry?.sessionId !== ctx.sessionManager.getSessionId()) return;
|
|
518
|
+
const model = await tracker.refresh();
|
|
519
|
+
if (changes === tracker) applyChanges(ctx, model);
|
|
476
520
|
};
|
|
477
521
|
const stopWatch = () => {
|
|
478
522
|
if (watch) clearInterval(watch);
|
|
479
523
|
watch = undefined;
|
|
480
524
|
};
|
|
525
|
+
const unsubscribeWorktrees = pi.events.on(SESSION_WORKTREE_CHANGED, (data) => {
|
|
526
|
+
if (!currentContext || !registry || (data as { sessionId?: string } | undefined)?.sessionId !== registry.sessionId) return;
|
|
527
|
+
void refreshChanges(currentContext);
|
|
528
|
+
});
|
|
529
|
+
pi.registerTool({
|
|
530
|
+
name: "session_worktree_register",
|
|
531
|
+
renderShell: "self",
|
|
532
|
+
label: "Register session worktree",
|
|
533
|
+
description: "Register a worktree used by this session, including earlier work or opaque shell use. Only the same Git clone is accepted. Shows ALL dirty files in that root, including preexisting and untracked files; never infers roots from shell commands or prose.",
|
|
534
|
+
parameters: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Worktree path to include in this session." } } } as never,
|
|
535
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
536
|
+
if (!registry || registry.sessionId !== ctx.sessionManager.getSessionId()) throw new Error("No active session worktree registry.");
|
|
537
|
+
const root = registry.register((params as { path: string }).path, "explicit");
|
|
538
|
+
await refreshChanges(ctx);
|
|
539
|
+
return { content: [{ type: "text", text: `Registered session worktree: ${root}` }], details: { root, sessionId: registry.sessionId } };
|
|
540
|
+
},
|
|
541
|
+
});
|
|
481
542
|
pi.on("session_start", async (_event, ctx) => {
|
|
543
|
+
stopWatch();
|
|
544
|
+
registry?.close();
|
|
545
|
+
pendingTools.clear();
|
|
546
|
+
currentContext = ctx;
|
|
547
|
+
changes = undefined;
|
|
548
|
+
registry = new SessionWorktreeRegistry(pi, ctx.sessionManager, ctx.cwd, deps.resolveWorktree);
|
|
549
|
+
registry.start();
|
|
550
|
+
const sessionRegistry = registry;
|
|
482
551
|
if (!ctx.hasUI) return;
|
|
483
|
-
changes = new
|
|
552
|
+
changes = new WorktreeChangesTracker(deps.gitRunner(ctx.cwd), deps.gitRunner, lineCounter, () => sessionRegistry.roots());
|
|
484
553
|
const tracker = changes;
|
|
485
554
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
486
|
-
renderHost = tui;
|
|
487
|
-
|
|
555
|
+
renderHost = { requestRender: () => tui.requestRender(), invalidateSidebar: () => invalidateSidebar(tui) };
|
|
556
|
+
const bottom = createShellBarComponent(pi, ctx, renderHost, theme, footerData, () => tracker.model.files.length, () => usage.get(ctx.model?.provider ?? ""));
|
|
557
|
+
// The Status card paints live session state that no event re-registers a
|
|
558
|
+
// part for: model, effort, context, cost, session name and extension
|
|
559
|
+
// statuses. The digest is what keeps the fullscreen memo honest, and it
|
|
560
|
+
// rebuilds the model exactly as the narrow bottom bar does every frame.
|
|
561
|
+
const footerModel = () => buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? "") });
|
|
562
|
+
const part = sidebarPart(tui, "footer", bottom, {
|
|
563
|
+
digest: () => JSON.stringify(footerModel()),
|
|
564
|
+
render: (width) => renderShellSidebarBar(footerModel(), theme, width),
|
|
565
|
+
invalidate() {},
|
|
566
|
+
});
|
|
567
|
+
const uninstall = installSidebar(tui, theme);
|
|
568
|
+
return { ...part, dispose() { uninstall(); part.dispose(); } };
|
|
488
569
|
});
|
|
489
570
|
void refreshUsage(ctx, true);
|
|
490
571
|
installPrompt(ctx, (created) => {
|
|
@@ -496,10 +577,11 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
496
577
|
ctx.ui.setWidget(
|
|
497
578
|
DEV_BINARY_WIDGET_KEY,
|
|
498
579
|
notice
|
|
499
|
-
? (_tui, theme) => spaced(cardComponent(devBinaryCard(notice), theme, { expanded: true
|
|
580
|
+
? (_tui, theme) => spaced(cardComponent(devBinaryCard(notice), theme, { expanded: true }))
|
|
500
581
|
: undefined,
|
|
501
582
|
);
|
|
502
583
|
await tracker.start();
|
|
584
|
+
if (changes !== tracker) return;
|
|
503
585
|
shown = "";
|
|
504
586
|
applyChanges(ctx, tracker.model);
|
|
505
587
|
stopWatch();
|
|
@@ -509,7 +591,15 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
509
591
|
watch.unref();
|
|
510
592
|
}
|
|
511
593
|
});
|
|
512
|
-
pi.on("session_shutdown", () =>
|
|
594
|
+
pi.on("session_shutdown", () => {
|
|
595
|
+
stopWatch();
|
|
596
|
+
registry?.close();
|
|
597
|
+
registry = undefined;
|
|
598
|
+
changes = undefined;
|
|
599
|
+
currentContext = undefined;
|
|
600
|
+
pendingTools.clear();
|
|
601
|
+
unsubscribeWorktrees();
|
|
602
|
+
});
|
|
513
603
|
const openChanges = async (ctx: ExtensionContext) => {
|
|
514
604
|
if (!changes) return;
|
|
515
605
|
const tracker = changes;
|
|
@@ -518,10 +608,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
518
608
|
ctx.ui.notify("No changes in the working tree.", "info");
|
|
519
609
|
return;
|
|
520
610
|
}
|
|
521
|
-
await showChangesOverlay(ctx,
|
|
611
|
+
await showChangesOverlay(ctx, { git: deps.gitRunner, worktrees: () => tracker.worktrees, refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env) });
|
|
522
612
|
};
|
|
523
613
|
pi.registerCommand(CHANGES_COMMAND_NAME, {
|
|
524
|
-
description: "
|
|
614
|
+
description: "Browse this session's registered dirty worktrees and their changes against HEAD. Press o on a file to open $EDITOR.",
|
|
525
615
|
handler: async (_args, ctx) => openChanges(ctx),
|
|
526
616
|
});
|
|
527
617
|
const shortcut = changesShortcut(env);
|
|
@@ -541,7 +631,20 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
|
|
|
541
631
|
await refreshChanges(ctx);
|
|
542
632
|
void refreshUsage(ctx, false);
|
|
543
633
|
});
|
|
544
|
-
pi.on("
|
|
634
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
635
|
+
pendingTools.set(event.toolCallId, { sessionId: ctx.sessionManager.getSessionId() });
|
|
636
|
+
});
|
|
637
|
+
pi.on("tool_result", (event, ctx) => {
|
|
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
|
+
}
|
|
545
648
|
await refreshChanges(ctx);
|
|
546
649
|
});
|
|
547
650
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { NativePointerRegion } from "../lib/native-pointer-region.ts";
|
|
4
|
+
import { sidebarPart } from "../lib/shell-sidebar.ts";
|
|
5
|
+
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
3
6
|
import {
|
|
4
7
|
applyTodo,
|
|
5
8
|
emptyTodo,
|
|
@@ -72,6 +75,7 @@ interface TodoSession {
|
|
|
72
75
|
clearOnNextTurn: boolean;
|
|
73
76
|
ui: ExtensionContext["ui"] | undefined;
|
|
74
77
|
host: { requestRender(): void } | undefined;
|
|
78
|
+
tui: TUI | undefined;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
function sessionKey(ctx: ExtensionContext): string {
|
|
@@ -87,13 +91,48 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
87
91
|
const key = sessionKey(ctx);
|
|
88
92
|
let current = sessions.get(key);
|
|
89
93
|
if (!current) {
|
|
90
|
-
current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined };
|
|
94
|
+
current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
|
|
91
95
|
sessions.set(key, current);
|
|
92
96
|
}
|
|
93
97
|
return current;
|
|
94
98
|
};
|
|
95
99
|
|
|
100
|
+
const toggle = (current: TodoSession) => {
|
|
101
|
+
current.collapsed = !current.collapsed;
|
|
102
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
103
|
+
current.host?.requestRender();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], scrollable: boolean, spacer: boolean): Component & { dispose(): void } => {
|
|
107
|
+
const card: Component = {
|
|
108
|
+
render(width: number) {
|
|
109
|
+
const lines = renderTodoCard(current.state, theme, width, {
|
|
110
|
+
collapsed: current.collapsed,
|
|
111
|
+
staleTurns: staleTurns(current.state, current.turn),
|
|
112
|
+
collapseKey,
|
|
113
|
+
...(scrollable ? { scrollable: true } : {}),
|
|
114
|
+
});
|
|
115
|
+
return spacer && lines.length > 0 ? [...lines, ""] : lines;
|
|
116
|
+
},
|
|
117
|
+
invalidate() {},
|
|
118
|
+
};
|
|
119
|
+
const region = new NativePointerRegion(card, {
|
|
120
|
+
onClick(event) {
|
|
121
|
+
if (event.button !== "left" || event.y !== 0) return undefined;
|
|
122
|
+
toggle(current);
|
|
123
|
+
return { handled: true, render: true };
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
render: (width) => region.render(width),
|
|
128
|
+
handleMouse: (event) => region.handleMouse(event),
|
|
129
|
+
invalidate: () => region.invalidate(),
|
|
130
|
+
dispose: () => region.dispose(),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
96
134
|
const show = (current: TodoSession) => {
|
|
135
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
97
136
|
if (!current.ui) return;
|
|
98
137
|
if (current.state.tasks.length === 0) {
|
|
99
138
|
current.ui.setWidget(WIDGET_KEY, undefined);
|
|
@@ -102,18 +141,14 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
102
141
|
const snapshot = current;
|
|
103
142
|
current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
104
143
|
snapshot.host = tui;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const lines = renderTodoCard(snapshot.state, theme, width, { collapsed: snapshot.collapsed, staleTurns: staleTurns(snapshot.state, snapshot.turn), collapseKey });
|
|
108
|
-
return lines.length === 0 ? [] : [...lines, ""];
|
|
109
|
-
},
|
|
110
|
-
invalidate() {},
|
|
111
|
-
};
|
|
144
|
+
snapshot.tui = tui;
|
|
145
|
+
return sidebarPart(tui, "todo", todoCard(snapshot, theme, false, true), todoCard(snapshot, theme, true, false));
|
|
112
146
|
});
|
|
113
147
|
};
|
|
114
148
|
|
|
115
149
|
pi.registerTool({
|
|
116
150
|
name: TODO_TOOL_NAME,
|
|
151
|
+
renderShell: "self",
|
|
117
152
|
label: "Todo",
|
|
118
153
|
description: "Plan and track multi-step work. Use write to set the whole list, update to move one task, add for a new one, clear to reset, list to read it back.",
|
|
119
154
|
promptSnippet: "Track multi-step work; rewrite the whole list as the plan changes",
|
|
@@ -139,6 +174,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
139
174
|
if (!result.error) {
|
|
140
175
|
current.state = result.state;
|
|
141
176
|
current.clearOnNextTurn = false;
|
|
177
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
142
178
|
}
|
|
143
179
|
return {
|
|
144
180
|
content: [{ type: "text", text: result.text }],
|
|
@@ -151,9 +187,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
151
187
|
pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
152
188
|
description: "Collapse or expand the todo list",
|
|
153
189
|
handler: async (ctx) => {
|
|
154
|
-
|
|
155
|
-
current.collapsed = !current.collapsed;
|
|
156
|
-
current.host?.requestRender();
|
|
190
|
+
toggle(session(ctx));
|
|
157
191
|
},
|
|
158
192
|
});
|
|
159
193
|
}
|
|
@@ -176,6 +210,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
176
210
|
pi.on("before_agent_start", (event, ctx) => {
|
|
177
211
|
const current = session(ctx);
|
|
178
212
|
current.turn += 1;
|
|
213
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
179
214
|
if (current.clearOnNextTurn) {
|
|
180
215
|
current.state = { ...current.state, tasks: [] };
|
|
181
216
|
current.clearOnNextTurn = false;
|
|
@@ -625,6 +625,7 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
|
|
|
625
625
|
|
|
626
626
|
pi.registerTool({
|
|
627
627
|
...registrationTool,
|
|
628
|
+
renderShell: "self",
|
|
628
629
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
629
630
|
const runtimeTool = getBuiltInTools(ctx.cwd)[toolName];
|
|
630
631
|
return runtimeTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { EFFORTS, ORCHESTRATOR_AGENT_CLASS, RuntimeMetrics, UNKNOWN_AGENT_CLASS, type FinalResponse, type TokenMeasurement } from "../lib/runtime-metrics.ts";
|
|
3
|
+
import { classifyPiCatalogName, lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
|
|
4
|
+
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, snapshotChildEvent, type ChildLaunchBucket } from "../lib/runtime-metrics-children.ts";
|
|
5
|
+
import { RuntimeMetricsAttempt } from "../lib/runtime-metrics-delivery.ts";
|
|
6
|
+
import { sendNativeRuntimeEvent, type NativeRuntimeTransportDeps } from "../lib/runtime-metrics-native.ts";
|
|
7
|
+
import { runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
|
|
8
|
+
|
|
9
|
+
/** Available final usage -> one deferred attempt -> discard. No history reads,
|
|
10
|
+
* cumulative session accounting, policy leases, delivery queue or retry. Print
|
|
11
|
+
* mode alone joins its accepted attempt for at most 1.5 seconds at shutdown.
|
|
12
|
+
* Pi hooks lack request correlation: latency and SDK-zero presence are unknown.
|
|
13
|
+
*/
|
|
14
|
+
export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
|
|
15
|
+
{ lookup = lookupPiCatalogName, classify = classifyPiCatalogName, native, send = sendNativeRuntimeEvent,
|
|
16
|
+
now = () => performance.now(), shutdownWaitMs = 1500 }:
|
|
17
|
+
{ lookup?: typeof lookupPiCatalogName; classify?: typeof classifyPiCatalogName; native?: NativeRuntimeTransportDeps;
|
|
18
|
+
send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
|
|
19
|
+
const allows = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && runtimeMetricsEnvAllows(env);
|
|
20
|
+
if (!allows()) return;
|
|
21
|
+
type Selection = Pick<FinalResponse, "selectedModelId" | "selectedProvider" | "effort">;
|
|
22
|
+
let selection: Selection | undefined;
|
|
23
|
+
let active = false;
|
|
24
|
+
let requestSeen = false;
|
|
25
|
+
let ambiguous = false;
|
|
26
|
+
let live: { id: string; started: number; ctx: ExtensionContext; attempt: RuntimeMetricsAttempt;
|
|
27
|
+
seen: WeakSet<object>; children: Set<string>; catalog: boolean } | undefined;
|
|
28
|
+
const missing = { state: "unavailable" } as const;
|
|
29
|
+
function invalidate() { selection = undefined; ambiguous = true; }
|
|
30
|
+
function dispose() { live?.attempt.dispose(); live = undefined; active = false; invalidate(); }
|
|
31
|
+
function current(ctx: ExtensionContext) {
|
|
32
|
+
if (!allows() || live?.id !== ctx.sessionManager.getSessionId()) dispose();
|
|
33
|
+
return live;
|
|
34
|
+
}
|
|
35
|
+
function refreshCatalog(owner: NonNullable<typeof live>, input: { provider: unknown; modelId: unknown }) {
|
|
36
|
+
void lookup(input).then(result => {
|
|
37
|
+
if (live === owner && result.classification === "catalog_public") owner.catalog = true;
|
|
38
|
+
}, () => { /* A later classification callback may retry the bounded load. */ });
|
|
39
|
+
}
|
|
40
|
+
function submit(owner: NonNullable<typeof live>, responses: FinalResponse[], launches?: ChildLaunchBucket[]) {
|
|
41
|
+
if (!responses.length || live !== owner || !current(owner.ctx)) return;
|
|
42
|
+
// Ephemeral event-local accounting only; source IDs never enter these rows.
|
|
43
|
+
const metrics = new RuntimeMetrics({ classifyModel: classify });
|
|
44
|
+
for (const [index, row] of responses.entries()) metrics.record({ ...row, responseId: String(index) });
|
|
45
|
+
const rows = metrics.snapshot();
|
|
46
|
+
if (!rows.length) return;
|
|
47
|
+
const cwd = owner.ctx.cwd;
|
|
48
|
+
owner.attempt.offer(async signal => {
|
|
49
|
+
if (live !== owner || !current(owner.ctx) || signal.aborted) return;
|
|
50
|
+
await send(rows, cwd, { ...native, launches, env, signal,
|
|
51
|
+
current: () => live === owner && current(owner.ctx) === owner });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const offChild = pi.events.on(CHILD_METRICS_EVENT, value => {
|
|
55
|
+
try {
|
|
56
|
+
const owner = live && current(live.ctx);
|
|
57
|
+
const event = snapshotChildEvent(value);
|
|
58
|
+
if (!owner || !event || event.parentSessionId !== owner.id || event.launchedAt < owner.started
|
|
59
|
+
|| owner.children.has(event.taskId) || owner.children.size >= 256) return;
|
|
60
|
+
owner.children.add(event.taskId); // Busy/failed completions stay consumed.
|
|
61
|
+
submit(owner, event.responses, [{ ...event.launch, evidence: "launch_configuration", launches: 1 }]);
|
|
62
|
+
} catch { /* No telemetry error enters the shared event bus. */ }
|
|
63
|
+
});
|
|
64
|
+
const offRevoke = pi.events.on(CHILD_METRICS_REVOKED, id => {
|
|
65
|
+
if (live?.id === id) {
|
|
66
|
+
live.attempt.dispose();
|
|
67
|
+
live.attempt = new RuntimeMetricsAttempt();
|
|
68
|
+
invalidate();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
pi.on("session_start", (_event, ctx) => {
|
|
72
|
+
dispose();
|
|
73
|
+
if (!allows()) return;
|
|
74
|
+
const owner = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
|
|
75
|
+
attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>(), catalog: false };
|
|
76
|
+
live = owner;
|
|
77
|
+
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
78
|
+
});
|
|
79
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
80
|
+
const teardown = () => { dispose(); offChild(); offRevoke(); };
|
|
81
|
+
const owner = live;
|
|
82
|
+
if (ctx.mode !== "print" || !owner) { teardown(); return; }
|
|
83
|
+
return owner.attempt.waitForSettled(shutdownWaitMs).finally(teardown);
|
|
84
|
+
});
|
|
85
|
+
pi.on("turn_start", () => { ambiguous = active; active = true; requestSeen = false; selection = undefined; });
|
|
86
|
+
pi.on("turn_end", () => { active = false; invalidate(); });
|
|
87
|
+
pi.on("agent_end", () => { active = false; invalidate(); });
|
|
88
|
+
pi.on("session_before_compact", invalidate);
|
|
89
|
+
pi.on("session_before_tree", invalidate);
|
|
90
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
91
|
+
const owner = current(ctx);
|
|
92
|
+
if (!owner) return;
|
|
93
|
+
if (requestSeen) invalidate();
|
|
94
|
+
requestSeen = true;
|
|
95
|
+
if (!active || ambiguous) return;
|
|
96
|
+
let effort: FinalResponse["effort"] = "unavailable";
|
|
97
|
+
try { const value = pi.getThinkingLevel(); if (EFFORTS.includes(value)) effort = value; } catch { /* No evidence. */ }
|
|
98
|
+
selection = { selectedProvider: owner.catalog ? ctx.model?.provider as FinalResponse["provider"] ?? "unknown" : "unknown",
|
|
99
|
+
selectedModelId: owner.catalog && typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
|
|
100
|
+
});
|
|
101
|
+
function token(value: unknown): TokenMeasurement {
|
|
102
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000
|
|
103
|
+
? { state: "reported", value } : missing;
|
|
104
|
+
}
|
|
105
|
+
pi.on("message_end", (event, ctx) => {
|
|
106
|
+
try {
|
|
107
|
+
const owner = current(ctx);
|
|
108
|
+
const message = event.message;
|
|
109
|
+
if (!owner || message.role !== "assistant" || message.stopReason === "pending" || message.stopReason === "deferred"
|
|
110
|
+
|| owner.seen.has(message)) return;
|
|
111
|
+
owner.seen.add(message);
|
|
112
|
+
refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
|
|
113
|
+
const selected = active && !ambiguous ? selection : undefined;
|
|
114
|
+
submit(owner, [{ kind: "final_assistant_response", responseId: "0",
|
|
115
|
+
selectedProvider: selected?.selectedProvider ?? "unknown", selectedModelId: selected?.selectedModelId,
|
|
116
|
+
effort: selected?.effort ?? "unavailable",
|
|
117
|
+
executor: env.GENTLE_PI_AGENTS_CHILD === undefined ? "orchestrator" : "unknown",
|
|
118
|
+
agentClass: env.GENTLE_PI_AGENTS_CHILD === undefined ? ORCHESTRATOR_AGENT_CLASS : UNKNOWN_AGENT_CLASS,
|
|
119
|
+
observedModelId: message.model, responseModelId: message.responseModel,
|
|
120
|
+
providerThinkingLevel: EFFORTS.includes(message.providerThinkingLevel as FinalResponse["effort"])
|
|
121
|
+
? message.providerThinkingLevel as FinalResponse["effort"] : "unavailable",
|
|
122
|
+
provider: message.provider as FinalResponse["provider"], modelFamily: "unknown",
|
|
123
|
+
error: message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "unknown" : "none",
|
|
124
|
+
tokens: { input: token(message.usage?.input), output: token(message.usage?.output), cacheRead: token(message.usage?.cacheRead),
|
|
125
|
+
cacheWrite: token(message.usage?.cacheWrite), reasoning: token(message.usage?.reasoning), totalTokens: token(message.usage?.totalTokens) },
|
|
126
|
+
responseHeadersMs: missing, fullResponseMs: missing }]);
|
|
127
|
+
invalidate();
|
|
128
|
+
} catch { /* Provider hooks never expose telemetry failures. */ }
|
|
129
|
+
});
|
|
130
|
+
}
|
package/extensions/sdd-init.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
import { applySavedModelConfig } from "./gentle-ai.ts";
|
|
10
|
-
import { ensureSddPreflight,
|
|
10
|
+
import { ensureSddPreflight, installPackageAssets } from "../lib/sdd-preflight.ts";
|
|
11
11
|
type ExtensionAPI = any;
|
|
12
12
|
|
|
13
13
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
@@ -775,7 +775,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
775
775
|
description:
|
|
776
776
|
"Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
|
|
777
777
|
handler: async (_args: unknown, ctx: any) => {
|
|
778
|
-
const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) =>
|
|
778
|
+
const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installPackageAssets(cwd, true, ["sdd"]), applyModelConfig: () => applySavedModelConfig(ctx) }, { promptFields: [] });
|
|
779
779
|
|
|
780
780
|
const detection = detectProject(ctx.cwd);
|
|
781
781
|
const testSummary = detection.testCommand
|