@tt-a1i/openpi 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,603 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants, lstatSync, readFileSync } from "node:fs";
4
+ import {
5
+ lstat,
6
+ mkdir,
7
+ open,
8
+ readFile,
9
+ type FileHandle,
10
+ } from "node:fs/promises";
11
+ import { basename, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import {
14
+ DefaultPackageManager,
15
+ getAgentDir,
16
+ SettingsManager,
17
+ type ProgressEvent,
18
+ } from "@earendil-works/pi-coding-agent";
19
+ import { sanitizeTerminalText } from "../shared/terminal-text.ts";
20
+
21
+ export const PI_INTERCOM_SOURCE = "npm:pi-intercom";
22
+ const PI_INTERCOM_CONFIG_LOCK = "config.json.openpi-install.lock";
23
+ const PI_INTERCOM_FS_HELPER = fileURLToPath(
24
+ new URL("./intercom-fs-helper.cjs", import.meta.url),
25
+ );
26
+
27
+ export interface PiIntercomStatus {
28
+ readonly configured: boolean;
29
+ readonly installed: boolean;
30
+ readonly active: boolean;
31
+ readonly version?: string;
32
+ readonly confirmSend?: boolean;
33
+ readonly inboundTrigger?: "always" | "replies" | "never";
34
+ readonly diagnostic?: string;
35
+ readonly reloadRequired?: boolean;
36
+ }
37
+
38
+ interface OptionalFile {
39
+ readonly exists: boolean;
40
+ readonly text?: string;
41
+ }
42
+
43
+ interface PreparedIntercomConfig {
44
+ readonly path: string;
45
+ readonly nextText: string;
46
+ readonly changed: boolean;
47
+ }
48
+
49
+ interface IntercomDirectoryGuard {
50
+ readonly directory: string;
51
+ readonly handle?: FileHandle;
52
+ readonly dev: number | bigint;
53
+ readonly ino: number | bigint;
54
+ }
55
+
56
+ interface PiIntercomInstallOptions {
57
+ readonly agentDir?: string;
58
+ /** Download/repair package files without making the package active. */
59
+ readonly install: (source: typeof PI_INTERCOM_SOURCE) => Promise<void>;
60
+ /** Persist the package source only after the safe config commit. */
61
+ readonly persist?: (source: typeof PI_INTERCOM_SOURCE) => Promise<void>;
62
+ }
63
+
64
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
65
+ typeof value === "object" && value !== null && !Array.isArray(value);
66
+
67
+ const isErrno = (error: unknown, code: string) =>
68
+ error instanceof Error && "code" in error && error.code === code;
69
+
70
+ const boundedError = (error: unknown) =>
71
+ sanitizeTerminalText(error instanceof Error ? error.message : String(error))
72
+ .replace(/\s+/gu, " ")
73
+ .trim()
74
+ .slice(0, 2_000);
75
+
76
+ export function isPiIntercomPackageSource(source: string) {
77
+ return /^npm:pi-intercom(?:@[^/\s]+)?$/.test(source.trim());
78
+ }
79
+
80
+ function intercomDirectory(agentDir: string) {
81
+ return join(agentDir, "intercom");
82
+ }
83
+
84
+ function intercomConfigPath(agentDir: string) {
85
+ return join(intercomDirectory(agentDir), "config.json");
86
+ }
87
+
88
+ async function ensureIntercomDirectory(agentDir: string) {
89
+ const directory = intercomDirectory(agentDir);
90
+ try {
91
+ const metadata = await lstat(directory);
92
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
93
+ throw new Error(
94
+ `Refusing non-directory or symlinked pi-intercom path at ${directory}.`,
95
+ );
96
+ }
97
+ } catch (error) {
98
+ if (!isErrno(error, "ENOENT")) throw error;
99
+ await mkdir(directory, { recursive: true, mode: 0o700 });
100
+ const metadata = await lstat(directory);
101
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
102
+ throw new Error(
103
+ `Refusing non-directory or symlinked pi-intercom path at ${directory}.`,
104
+ );
105
+ }
106
+ }
107
+ return directory;
108
+ }
109
+
110
+ const sameIdentity = (
111
+ left: { dev: number | bigint; ino: number | bigint },
112
+ right: { dev: number | bigint; ino: number | bigint },
113
+ ) => left.dev === right.dev && left.ino === right.ino;
114
+
115
+ async function openIntercomDirectoryGuard(agentDir: string) {
116
+ const directory = await ensureIntercomDirectory(agentDir);
117
+ const current = await lstat(directory, { bigint: true });
118
+ if (process.platform === "win32") {
119
+ return {
120
+ directory,
121
+ dev: current.dev,
122
+ ino: current.ino,
123
+ } satisfies IntercomDirectoryGuard;
124
+ }
125
+ const flags =
126
+ constants.O_RDONLY |
127
+ (constants.O_DIRECTORY ?? 0) |
128
+ (constants.O_NOFOLLOW ?? 0);
129
+ const handle = await open(directory, flags);
130
+ try {
131
+ const identity = await handle.stat({ bigint: true });
132
+ const latest = await lstat(directory, { bigint: true });
133
+ if (
134
+ !identity.isDirectory() ||
135
+ !latest.isDirectory() ||
136
+ latest.isSymbolicLink() ||
137
+ !sameIdentity(identity, latest)
138
+ ) {
139
+ throw new Error(
140
+ `pi-intercom directory identity changed while opening ${directory}.`,
141
+ );
142
+ }
143
+ return {
144
+ directory,
145
+ handle,
146
+ dev: identity.dev,
147
+ ino: identity.ino,
148
+ } satisfies IntercomDirectoryGuard;
149
+ } catch (error) {
150
+ await handle.close().catch(() => undefined);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ async function assertIntercomDirectoryIdentity(guard: IntercomDirectoryGuard) {
156
+ const current = await lstat(guard.directory, { bigint: true });
157
+ const held = guard.handle
158
+ ? await guard.handle.stat({ bigint: true })
159
+ : { dev: guard.dev, ino: guard.ino, isDirectory: () => true };
160
+ if (
161
+ !held.isDirectory() ||
162
+ !current.isDirectory() ||
163
+ current.isSymbolicLink() ||
164
+ !sameIdentity(held, current) ||
165
+ held.dev !== guard.dev ||
166
+ held.ino !== guard.ino
167
+ ) {
168
+ throw new Error(
169
+ `pi-intercom directory identity changed during installation at ${guard.directory}.`,
170
+ );
171
+ }
172
+ }
173
+
174
+ async function readOptionalFile(path: string): Promise<OptionalFile> {
175
+ try {
176
+ const [text, metadata] = await Promise.all([
177
+ readFile(path, "utf8"),
178
+ lstat(path),
179
+ ]);
180
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
181
+ throw new Error(
182
+ `Refusing non-regular pi-intercom config path at ${path}.`,
183
+ );
184
+ }
185
+ return { exists: true, text };
186
+ } catch (error) {
187
+ if (isErrno(error, "ENOENT")) return { exists: false };
188
+ throw error;
189
+ }
190
+ }
191
+
192
+ function parseIntercomConfig(file: OptionalFile) {
193
+ if (!file.exists) return {};
194
+
195
+ let value: unknown;
196
+ try {
197
+ value = JSON.parse(file.text!);
198
+ } catch (error) {
199
+ throw new Error(
200
+ `Refusing to overwrite invalid pi-intercom config (${boundedError(error)}).`,
201
+ );
202
+ }
203
+ if (!isRecord(value)) {
204
+ throw new Error("Refusing to overwrite non-object pi-intercom config.");
205
+ }
206
+ if (
207
+ Object.hasOwn(value, "confirmSend") &&
208
+ typeof value.confirmSend !== "boolean"
209
+ ) {
210
+ throw new Error(
211
+ 'Refusing to overwrite pi-intercom config: "confirmSend" must be boolean.',
212
+ );
213
+ }
214
+ if (
215
+ Object.hasOwn(value, "inboundTrigger") &&
216
+ value.inboundTrigger !== "always" &&
217
+ value.inboundTrigger !== "replies" &&
218
+ value.inboundTrigger !== "never"
219
+ ) {
220
+ throw new Error(
221
+ 'Refusing to overwrite pi-intercom config: "inboundTrigger" must be "always", "replies", or "never".',
222
+ );
223
+ }
224
+ return value;
225
+ }
226
+
227
+ export async function preparePiIntercomSafeDefaults(
228
+ agentDir = getAgentDir(),
229
+ guard?: IntercomDirectoryGuard,
230
+ ): Promise<PreparedIntercomConfig> {
231
+ if (guard) await assertIntercomDirectoryIdentity(guard);
232
+ else await ensureIntercomDirectory(agentDir);
233
+ const path = intercomConfigPath(agentDir);
234
+ const original = await readOptionalFile(path);
235
+ if (guard) await assertIntercomDirectoryIdentity(guard);
236
+ const current = parseIntercomConfig(original);
237
+ if (original.exists) {
238
+ const missing = [
239
+ ...(!Object.hasOwn(current, "confirmSend") ? ["confirmSend"] : []),
240
+ ...(!Object.hasOwn(current, "inboundTrigger") ? ["inboundTrigger"] : []),
241
+ ];
242
+ if (missing.length > 0) {
243
+ throw new Error(
244
+ `Existing pi-intercom config is missing ${missing.join(" and ")}; OpenPI will not rewrite an existing preference file. Add confirmSend=true and inboundTrigger=\"replies\", then retry.`,
245
+ );
246
+ }
247
+ return { path, nextText: original.text!, changed: false };
248
+ }
249
+
250
+ return {
251
+ path,
252
+ nextText: `${JSON.stringify(
253
+ { confirmSend: true, inboundTrigger: "replies" },
254
+ null,
255
+ 2,
256
+ )}\n`,
257
+ changed: true,
258
+ };
259
+ }
260
+
261
+ function helperRuntime() {
262
+ const executable = basename(process.execPath).toLowerCase();
263
+ return executable === "node" ||
264
+ executable === "node.exe" ||
265
+ executable === "bun" ||
266
+ executable === "bun.exe"
267
+ ? process.execPath
268
+ : "node";
269
+ }
270
+
271
+ function runIntercomDirectoryHelper(options: {
272
+ readonly guard: IntercomDirectoryGuard;
273
+ readonly operation: "create" | "remove-owned";
274
+ readonly name: "config.json" | typeof PI_INTERCOM_CONFIG_LOCK;
275
+ readonly payload: string;
276
+ }) {
277
+ const encoded = Buffer.from(options.payload, "utf8").toString("base64");
278
+ return new Promise<void>((resolve, reject) => {
279
+ execFile(
280
+ helperRuntime(),
281
+ [
282
+ PI_INTERCOM_FS_HELPER,
283
+ options.operation,
284
+ String(options.guard.dev),
285
+ String(options.guard.ino),
286
+ options.name,
287
+ encoded,
288
+ ],
289
+ {
290
+ cwd: options.guard.directory,
291
+ encoding: "utf8",
292
+ env: { PATH: process.env.PATH ?? "" },
293
+ maxBuffer: 16 * 1_024,
294
+ timeout: 5_000,
295
+ windowsHide: true,
296
+ },
297
+ (error, _stdout, stderr) => {
298
+ if (!error) {
299
+ resolve();
300
+ return;
301
+ }
302
+ const marker = stderr.match(/OPENPI:([A-Z]+):([^\r\n]*)/u);
303
+ const failure = new Error(
304
+ marker
305
+ ? `pi-intercom filesystem helper refused ${options.operation}: ${boundedError(marker[2])}`
306
+ : `pi-intercom filesystem helper failed: ${boundedError(error)}`,
307
+ { cause: error },
308
+ );
309
+ if (marker?.[1] === "EEXIST") {
310
+ Object.assign(failure, { code: "EEXIST" });
311
+ }
312
+ reject(failure);
313
+ },
314
+ );
315
+ });
316
+ }
317
+
318
+ async function commitPreparedConfig(
319
+ prepared: PreparedIntercomConfig,
320
+ guard: IntercomDirectoryGuard,
321
+ ) {
322
+ if (!prepared.changed) return false;
323
+ try {
324
+ if (prepared.path !== join(guard.directory, "config.json")) {
325
+ throw new Error("Refusing unexpected pi-intercom config path.");
326
+ }
327
+ await assertIntercomDirectoryIdentity(guard);
328
+ await runIntercomDirectoryHelper({
329
+ guard,
330
+ operation: "create",
331
+ name: "config.json",
332
+ payload: prepared.nextText,
333
+ });
334
+ await assertIntercomDirectoryIdentity(guard);
335
+ return true;
336
+ } catch (error) {
337
+ if (isErrno(error, "EEXIST")) {
338
+ throw new Error(
339
+ "pi-intercom config appeared while OpenPI was preparing the installation; retry instead of overwriting it.",
340
+ );
341
+ }
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ async function withPiIntercomConfigLock<A>(
347
+ agentDir: string,
348
+ action: (guard: IntercomDirectoryGuard) => Promise<A>,
349
+ ) {
350
+ const guard = await openIntercomDirectoryGuard(agentDir);
351
+ const lockPath = join(guard.directory, PI_INTERCOM_CONFIG_LOCK);
352
+ const token = `${process.pid}:${randomUUID()}\n`;
353
+ try {
354
+ await assertIntercomDirectoryIdentity(guard);
355
+ await runIntercomDirectoryHelper({
356
+ guard,
357
+ operation: "create",
358
+ name: PI_INTERCOM_CONFIG_LOCK,
359
+ payload: token,
360
+ });
361
+ await assertIntercomDirectoryIdentity(guard);
362
+ } catch (error) {
363
+ await guard.handle?.close().catch(() => undefined);
364
+ if (isErrno(error, "EEXIST")) {
365
+ throw new Error(
366
+ `Another OpenPI pi-intercom installation is active, or a prior process left ${lockPath}. Retry after the active setup finishes; remove a stale lock only after confirming no setup is running.`,
367
+ );
368
+ }
369
+ throw error;
370
+ }
371
+
372
+ try {
373
+ try {
374
+ return await action(guard);
375
+ } finally {
376
+ await assertIntercomDirectoryIdentity(guard);
377
+ try {
378
+ await runIntercomDirectoryHelper({
379
+ guard,
380
+ operation: "remove-owned",
381
+ name: PI_INTERCOM_CONFIG_LOCK,
382
+ payload: token,
383
+ });
384
+ } catch (error) {
385
+ throw new Error(
386
+ `Refusing uncertain pi-intercom install-lock cleanup at ${lockPath}: ${boundedError(error)}`,
387
+ );
388
+ }
389
+ }
390
+ } finally {
391
+ await guard.handle?.close().catch(() => undefined);
392
+ }
393
+ }
394
+
395
+ export async function installPiIntercomSafely(
396
+ options: PiIntercomInstallOptions,
397
+ ) {
398
+ const agentDir = options.agentDir ?? getAgentDir();
399
+ return withPiIntercomConfigLock(agentDir, async (guard) => {
400
+ // Validate before spending network/disk work, but re-read after the package
401
+ // download so a concurrent manual edit is never replaced from a stale
402
+ // pre-download snapshot.
403
+ await preparePiIntercomSafeDefaults(agentDir, guard);
404
+ try {
405
+ await options.install(PI_INTERCOM_SOURCE);
406
+ } catch (error) {
407
+ throw new Error(boundedError(error), { cause: error });
408
+ }
409
+
410
+ const prepared = await preparePiIntercomSafeDefaults(agentDir, guard);
411
+ const committed = await commitPreparedConfig(prepared, guard);
412
+ try {
413
+ await options.persist?.(PI_INTERCOM_SOURCE);
414
+ } catch (error) {
415
+ const retained = committed
416
+ ? " Safe defaults were retained because package activation may have reached disk."
417
+ : " Existing pi-intercom preferences were preserved.";
418
+ throw new Error(`${boundedError(error)}${retained}`, { cause: error });
419
+ }
420
+ });
421
+ }
422
+
423
+ function inspectInstalledPackage(installedPath: string | undefined) {
424
+ if (!installedPath) return { installed: false as const };
425
+ try {
426
+ const directoryMetadata = lstatSync(installedPath);
427
+ if (
428
+ !directoryMetadata.isDirectory() ||
429
+ directoryMetadata.isSymbolicLink()
430
+ ) {
431
+ throw new Error("installed package directory is not a regular directory");
432
+ }
433
+ const manifestPath = join(installedPath, "package.json");
434
+ const manifestMetadata = lstatSync(manifestPath);
435
+ if (!manifestMetadata.isFile() || manifestMetadata.isSymbolicLink()) {
436
+ throw new Error("installed package manifest is not a regular file");
437
+ }
438
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown;
439
+ if (!isRecord(manifest) || manifest.name !== "pi-intercom") {
440
+ return {
441
+ installed: false as const,
442
+ diagnostic: `Installed package identity mismatch at ${installedPath}.`,
443
+ };
444
+ }
445
+ return {
446
+ installed: true as const,
447
+ ...(typeof manifest.version === "string"
448
+ ? { version: manifest.version }
449
+ : {}),
450
+ };
451
+ } catch (error) {
452
+ return {
453
+ installed: false as const,
454
+ diagnostic: `Cannot verify installed pi-intercom package at ${installedPath}: ${boundedError(error)}`,
455
+ };
456
+ }
457
+ }
458
+
459
+ function readEffectiveSafety(agentDir: string):
460
+ | Pick<PiIntercomStatus, "confirmSend" | "inboundTrigger">
461
+ | {
462
+ diagnostic: string;
463
+ } {
464
+ try {
465
+ const directoryMetadata = lstatSync(intercomDirectory(agentDir));
466
+ if (
467
+ !directoryMetadata.isDirectory() ||
468
+ directoryMetadata.isSymbolicLink()
469
+ ) {
470
+ throw new Error("Refusing symlinked pi-intercom config directory.");
471
+ }
472
+ const path = intercomConfigPath(agentDir);
473
+ const metadata = lstatSync(path);
474
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
475
+ throw new Error("Refusing non-regular pi-intercom config file.");
476
+ }
477
+ const value = parseIntercomConfig({
478
+ exists: true,
479
+ text: readFileSync(path, "utf8"),
480
+ });
481
+ return {
482
+ confirmSend:
483
+ typeof value.confirmSend === "boolean" ? value.confirmSend : false,
484
+ inboundTrigger:
485
+ value.inboundTrigger === "always" ||
486
+ value.inboundTrigger === "replies" ||
487
+ value.inboundTrigger === "never"
488
+ ? value.inboundTrigger
489
+ : ("always" as const),
490
+ };
491
+ } catch (error) {
492
+ return isErrno(error, "ENOENT")
493
+ ? { confirmSend: false, inboundTrigger: "always" as const }
494
+ : { diagnostic: boundedError(error) };
495
+ }
496
+ }
497
+
498
+ function settingsErrorsMessage(
499
+ errors: ReturnType<SettingsManager["drainErrors"]>,
500
+ ) {
501
+ return errors
502
+ .map(({ scope, error }) => `${scope}: ${boundedError(error)}`)
503
+ .join("; ")
504
+ .slice(0, 2_000);
505
+ }
506
+
507
+ export function inspectPiIntercom(options: {
508
+ readonly cwd: string;
509
+ readonly active: boolean;
510
+ readonly agentDir?: string;
511
+ }): PiIntercomStatus {
512
+ const agentDir = options.agentDir ?? getAgentDir();
513
+ const settingsManager = SettingsManager.create(options.cwd, agentDir, {
514
+ projectTrusted: false,
515
+ });
516
+ const settingsErrors = settingsManager.drainErrors();
517
+ if (settingsErrors.length > 0) {
518
+ return {
519
+ configured: false,
520
+ installed: false,
521
+ active: options.active,
522
+ diagnostic: settingsErrorsMessage(settingsErrors),
523
+ };
524
+ }
525
+
526
+ const packageManager = new DefaultPackageManager({
527
+ cwd: options.cwd,
528
+ agentDir,
529
+ settingsManager,
530
+ });
531
+ const configuredPackage = packageManager
532
+ .listConfiguredPackages()
533
+ .find(({ source }) => isPiIntercomPackageSource(source));
534
+ const safety = readEffectiveSafety(agentDir);
535
+ const installed = inspectInstalledPackage(configuredPackage?.installedPath);
536
+ return {
537
+ configured: Boolean(configuredPackage),
538
+ active: options.active,
539
+ ...installed,
540
+ ...safety,
541
+ };
542
+ }
543
+
544
+ export async function installPiIntercom(options: {
545
+ readonly cwd: string;
546
+ readonly agentDir?: string;
547
+ readonly onProgress?: (event: ProgressEvent) => void;
548
+ }) {
549
+ const agentDir = options.agentDir ?? getAgentDir();
550
+ const settingsManager = SettingsManager.create(options.cwd, agentDir, {
551
+ projectTrusted: false,
552
+ });
553
+ const initialErrors = settingsManager.drainErrors();
554
+ if (initialErrors.length > 0) {
555
+ throw new Error(
556
+ `Cannot install pi-intercom while Pi settings are invalid: ${settingsErrorsMessage(initialErrors)}`,
557
+ );
558
+ }
559
+
560
+ const packageManager = new DefaultPackageManager({
561
+ cwd: options.cwd,
562
+ agentDir,
563
+ settingsManager,
564
+ });
565
+ packageManager.setProgressCallback(options.onProgress);
566
+ await installPiIntercomSafely({
567
+ agentDir,
568
+ install: (source) => packageManager.install(source),
569
+ persist: async (source) => {
570
+ packageManager.addSourceToSettings(source);
571
+ await settingsManager.flush();
572
+ const errors = settingsManager.drainErrors();
573
+ if (errors.length > 0) {
574
+ throw new Error(
575
+ `Pi could not persist the package setting: ${settingsErrorsMessage(errors)}`,
576
+ );
577
+ }
578
+ },
579
+ });
580
+ }
581
+
582
+ export function formatPiIntercomStatus(status: PiIntercomStatus) {
583
+ if (status.diagnostic) {
584
+ return `Intercom: unavailable (${boundedError(status.diagnostic)})`;
585
+ }
586
+ if (!status.configured && !status.active) {
587
+ return "Intercom: not installed · optional setup component";
588
+ }
589
+
590
+ const state = status.active
591
+ ? "active"
592
+ : status.installed
593
+ ? status.reloadRequired
594
+ ? "installed · /reload required"
595
+ : "installed · inactive or filtered"
596
+ : "configured · package files missing";
597
+ const version = status.version ? ` ${status.version}` : "";
598
+ const safety =
599
+ status.confirmSend === undefined || status.inboundTrigger === undefined
600
+ ? ""
601
+ : ` · confirmSend ${status.confirmSend ? "on" : "off"} · inboundTrigger ${status.inboundTrigger}`;
602
+ return `Intercom:${version} · ${state} · parent-only${safety}`;
603
+ }
@@ -267,11 +267,12 @@ async function waitUntil(
267
267
  const remaining = Math.max(0, deadline - Date.now());
268
268
  let timer: ReturnType<typeof setTimeout> | undefined;
269
269
  const timeout = new Promise<{ error: string; timedOut: true }>((resolve) => {
270
+ // This timer owns the awaited deadline contract. Keep it referenced so a
271
+ // short-lived Node 22 process cannot exit with the promise still pending.
270
272
  timer = setTimeout(
271
273
  () => resolve({ error: `${label} timed out`, timedOut: true }),
272
274
  remaining,
273
275
  );
274
- timer.unref?.();
275
276
  });
276
277
  const completed = operation.then(
277
278
  () => ({ timedOut: false as const }),
@@ -929,7 +929,10 @@ export async function saveSetupConfig(config: MyPiSetupConfig) {
929
929
  });
930
930
  }
931
931
 
932
- export function formatSetupConfig(config = loadSetupConfig()) {
932
+ export function formatSetupConfig(
933
+ config = loadSetupConfig(),
934
+ integrationLines: readonly string[] = [],
935
+ ) {
933
936
  const suggestionModel = config.suggestions.model;
934
937
  const suggestions =
935
938
  !config.suggestions.enabled || !suggestionModel
@@ -947,6 +950,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
947
950
  `Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
948
951
  `Post-edit command: ${config.postEdit.command ? config.postEdit.command : "off"}`,
949
952
  `Agent role models (Subagents + Workflows): ${SUBAGENT_ROLE_NAMES.map((role) => `${role} ${config.subagents.roleModels[role] ? `${config.subagents.roleModels[role].provider}/${config.subagents.roleModels[role].model}` : "inherit"}`).join(" · ")}`,
953
+ ...integrationLines,
950
954
  ].join("\n");
951
955
  }
952
956
 
@@ -633,7 +633,7 @@ export default function (pi: ExtensionAPI) {
633
633
  ? planModeChildTools(declaredChildTools)
634
634
  : declaredChildTools;
635
635
  const childTools = effectiveChildToolAllowlist(requestedChildTools);
636
- // Read at spawn time so `/my-pi-setup` changes affect the next child
636
+ // Read at spawn time so `/openpi-setup` changes affect the next child
637
637
  // without reloading this extension. Undefined preserves parent-model
638
638
  // inheritance in the backend.
639
639
  const model = selectSubagentModel(
@@ -1,4 +1,8 @@
1
- import type { TranscriptEntry, WorkflowDetails } from "./model.ts";
1
+ import {
2
+ refreshWorkflowGraph,
3
+ type TranscriptEntry,
4
+ type WorkflowDetails,
5
+ } from "./model.ts";
2
6
  import {
3
7
  boundedJournal,
4
8
  parseJournal,
@@ -101,6 +105,7 @@ export function persistWorkflowJson(
101
105
  details: WorkflowDetails,
102
106
  journal?: readonly JournalEntry[],
103
107
  ) {
108
+ refreshWorkflowGraph(details);
104
109
  const transcripts = Object.fromEntries(
105
110
  details.agents.map((agent) => [
106
111
  agent.index,