@tt-a1i/openpi 0.3.1 → 0.5.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.
Files changed (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -162,6 +162,30 @@ export function filterSessionInfos(
162
162
  );
163
163
  }
164
164
 
165
+ /**
166
+ * Mirror pi-tui SelectList's centered viewport so background stats work is
167
+ * limited to rows the picker can actually render. Keep the formula locked by
168
+ * tests because SelectList does not expose its visible range.
169
+ */
170
+ export function selectSessionStatsWindow(
171
+ sessions: readonly SessionInfoLike[],
172
+ selectedPath: string,
173
+ maxVisible: number,
174
+ ) {
175
+ if (sessions.length === 0) return [];
176
+ const visible = Math.max(1, Math.min(maxVisible, sessions.length));
177
+ const found = sessions.findIndex((session) => session.path === selectedPath);
178
+ const selectedIndex = found >= 0 ? found : 0;
179
+ const startIndex = Math.max(
180
+ 0,
181
+ Math.min(
182
+ selectedIndex - Math.floor(visible / 2),
183
+ sessions.length - visible,
184
+ ),
185
+ );
186
+ return sessions.slice(startIndex, startIndex + visible);
187
+ }
188
+
165
189
  export function getSessionPaneLayout(width: number): SessionPaneLayout {
166
190
  if (width < 80) {
167
191
  return { mode: "single", listWidth: width, previewWidth: 0 };
@@ -340,12 +364,21 @@ function messageToBlocks(message: PreviewMessageLike): PreviewBlock[] {
340
364
  export function buildSessionPreview(
341
365
  session: SessionInfoLike,
342
366
  messages: PreviewMessageLike[],
343
- options: { maxMessages?: number } = {},
367
+ options: {
368
+ maxMessages?: number;
369
+ totalMessages?: number;
370
+ truncatedBytes?: number;
371
+ } = {},
344
372
  ): SessionPreview {
345
373
  const maxMessages = options.maxMessages ?? 80;
346
374
  const blocks: PreviewBlock[] = [];
347
- const omitted = Math.max(0, messages.length - maxMessages);
348
- const visibleMessages = omitted > 0 ? messages.slice(-maxMessages) : messages;
375
+ const visibleMessages =
376
+ messages.length > maxMessages ? messages.slice(-maxMessages) : messages;
377
+ const totalMessages = Math.max(
378
+ visibleMessages.length,
379
+ options.totalMessages ?? messages.length,
380
+ );
381
+ const omitted = Math.max(0, totalMessages - visibleMessages.length);
349
382
 
350
383
  if (omitted > 0) {
351
384
  blocks.push({
@@ -353,12 +386,18 @@ export function buildSessionPreview(
353
386
  text: `… ${omitted} earlier messages omitted`,
354
387
  });
355
388
  }
389
+ if ((options.truncatedBytes ?? 0) > 0) {
390
+ blocks.push({
391
+ kind: "notice",
392
+ text: `… ${options.truncatedBytes} bytes of preview content omitted`,
393
+ });
394
+ }
356
395
 
357
396
  for (const message of visibleMessages) {
358
397
  blocks.push(...messageToBlocks(message));
359
398
  }
360
399
 
361
- const messageCount = session.messageCount ?? messages.length;
400
+ const messageCount = session.messageCount ?? totalMessages;
362
401
  return {
363
402
  title: buildSessionLabel(session),
364
403
  subtitle: `${formatTimestamp(session.modified)} · ${messageCount} messages · ${cleanDisplayLine(session.cwd)}`,
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ExtensionAPI,
3
3
  ExtensionCommandContext,
4
+ ExtensionContext,
4
5
  } from "@earendil-works/pi-coding-agent";
5
6
  import { StringEnum } from "@earendil-works/pi-ai";
6
7
  import { Type } from "typebox";
@@ -9,18 +10,15 @@ import {
9
10
  type SubagentRoleModel,
10
11
  type SubagentRoleModels,
11
12
  } from "../shared/subagent-roles.ts";
12
- import { sanitizeTerminalText } from "../shared/terminal-text.ts";
13
13
  import {
14
14
  OPENPI_SETUP_EPISODE_CHANNEL,
15
15
  type OpenPiSetupEpisodeState,
16
16
  } from "../shared/setup-episode-state.ts";
17
- import { patchOwnedTools } from "../shared/tool-surface.ts";
18
17
  import {
19
- formatPiIntercomStatus,
20
- inspectPiIntercom,
21
- installPiIntercom,
22
- type PiIntercomStatus,
23
- } from "./intercom.ts";
18
+ isOwnedToolActive,
19
+ isOwnedToolAvailable,
20
+ patchOwnedTools,
21
+ } from "../shared/tool-surface.ts";
24
22
  import {
25
23
  applyFooterConfig,
26
24
  CAPABILITY_DISCOVERY_MODES,
@@ -132,12 +130,11 @@ export function buildInteractiveSetupPrompt(options: {
132
130
  "- Capability discovery: explicit is the safe default and keeps OpenPI model tools absent until the user asks for a capability. adaptive is opt-in and keeps only the small openpi_load_tools gateway visible, allowing the model to load Subagents, Workflows, background terminals, structured search, or Session tracking when it judges them useful. Loaded groups remain session-stable, and normal permission, concurrency, and workflow limits still apply.",
133
131
  "- Next-action suggestions: disabled, or model-generated after a fully settled main-agent run. A suggestion appears as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. Right accepts it without submitting, and any other editor input dismisses it. Enabling requires an available provider/model and reasoning level and adds one small model call per settled run.",
134
132
  "- Workflow fan-out: concurrency controls simultaneous agents and resource pressure; max agent calls controls the total capacity of one workflow. Valid ranges are 1-64 and 1-1024.",
135
- "- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with cwd/git/pr on the left and model/context/cost on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Nerd Font only affects powerline separator glyphs; text stays readable without it. Changes apply immediately in the active TUI session.",
133
+ "- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with model/context on the left and git/pr/cwd on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Footer metrics use Codicon outline glyphs for model, context, and directory; a Nerd Font renders them as designed while the text stays readable without it. Changes apply immediately in the active TUI session.",
136
134
  "- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
137
135
  "- Post-edit command: one optional shell command (maximum 500 characters) run in the background after a turn with successful Write/Edit operations (e.g. `npm run format`). Off by default, interactive TUI sessions only, failures surface as a notification. This is a single command, not an event-hook system.",
138
- "- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use folded previews. Ctrl+O expands compact output by default. Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
136
+ "- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact; all three default to compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use one-line semantic activity summaries. Read, grep, find, and ls use the same compact activity-row projection. Ctrl+O restores Pi's native full arguments, output, errors, diffs, and timing. Recommend compact for users who scan activity first and inspect evidence on demand.",
139
137
  "- Agent role models: built-in explorer, implementer, reviewer, and advisor roles are shared by subagent_spawn and workflow agent_type, and inherit the parent model by default. Assign only an available registry model to an individual role when needed; clearing that role returns it to inheritance. Custom agent-type files still override a built-in role's complete definition.",
140
- "- Intercom: optional cross-session messaging is installed only after a native setup confirmation. It stays parent-only; Direct/Workflow children and Replay cannot use it. The status above is informational for this model-guided step—do not install packages or edit its config yourself.",
141
138
  "",
142
139
  "Natural-language configuration examples the user might ask for:",
143
140
  '- "let the model discover OpenPI capabilities when useful" → capability_discovery=adaptive',
@@ -151,16 +148,10 @@ export function buildInteractiveSetupPrompt(options: {
151
148
  '- "make explorer use my available fast model" → subagent_role_models={explorer:{provider:"…",model:"…"}}',
152
149
  '- "make explorer inherit again" → subagent_role_models={explorer:null}',
153
150
  "",
154
- "Use ask_user for the decision instead of merely printing instructions. Put the recommended choice first. Do not change configuration until the choices are clear. Then call configure_my_pi_setup at most once with the final requested changes, preserving everything else. Do not edit configuration files directly.",
151
+ "configure_my_pi_setup is available only for this setup run. If the run settles without a successful apply, the writer is hidden and a later change requires /openpi-setup <request>. Use ask_user for the decision instead of merely printing instructions. Put the recommended choice first. Do not change configuration until the choices are clear. Then call configure_my_pi_setup at most once with the final requested changes, preserving everything else. Do not edit configuration files directly.",
155
152
  ];
156
153
  }
157
154
 
158
- const safeSetupNotice = (value: unknown, maximum = 500) =>
159
- sanitizeTerminalText(value instanceof Error ? value.message : String(value))
160
- .replace(/\s+/gu, " ")
161
- .trim()
162
- .slice(0, maximum);
163
-
164
155
  export function buildSetupSuccessText(
165
156
  currentConfiguration: string,
166
157
  normalizationNote = "",
@@ -171,87 +162,12 @@ export function buildSetupSuccessText(
171
162
  ].join(" ");
172
163
  }
173
164
 
174
- export function shouldOfferPiIntercom(options: {
175
- readonly request: string;
176
- readonly status: PiIntercomStatus;
177
- readonly mode: ExtensionCommandContext["mode"];
178
- readonly idle: boolean;
179
- }) {
180
- return (
181
- !options.request &&
182
- !options.status.active &&
183
- !options.status.installed &&
184
- !options.status.diagnostic &&
185
- options.mode === "tui" &&
186
- options.idle
187
- );
188
- }
189
-
190
- async function maybeOfferPiIntercom(
191
- ctx: ExtensionCommandContext,
192
- status: PiIntercomStatus,
193
- request: string,
194
- ) {
195
- if (
196
- !shouldOfferPiIntercom({
197
- request,
198
- status,
199
- mode: ctx.mode,
200
- idle: ctx.isIdle(),
201
- })
202
- ) {
203
- return status;
204
- }
205
-
206
- const accepted = await ctx.ui.confirm(
207
- status.configured
208
- ? "Repair optional pi-intercom integration?"
209
- : "Install optional pi-intercom integration?",
210
- [
211
- "pi-intercom enables cross-session messaging through a local IPC broker.",
212
- "Like every Pi package, it runs with full system access.",
213
- "OpenPI will install npm:pi-intercom globally. A new private config gets safe defaults; an existing preference file is never rewritten and must already define both fields:",
214
- "• confirmSend: true",
215
- '• inboundTrigger: "replies"',
216
- "It remains parent-only and activates after /reload.",
217
- ].join("\n"),
218
- );
219
- if (!accepted) return status;
220
-
221
- ctx.ui.setWorkingMessage("Installing optional pi-intercom integration...");
222
- try {
223
- await installPiIntercom({
224
- cwd: ctx.cwd,
225
- onProgress: (event) =>
226
- ctx.ui.setWorkingMessage(
227
- safeSetupNotice(
228
- event.message ?? "Installing optional pi-intercom integration...",
229
- 200,
230
- ),
231
- ),
232
- });
233
- const installed = inspectPiIntercom({
234
- cwd: ctx.cwd,
235
- active: false,
236
- });
237
- const next = { ...installed, reloadRequired: true };
238
- ctx.ui.notify(
239
- "pi-intercom installed with existing preferences preserved or a new safe config created. Run /reload after setup to activate it.",
240
- "info",
241
- );
242
- return next;
243
- } catch (error) {
244
- ctx.ui.notify(
245
- `pi-intercom was not enabled: ${safeSetupNotice(error)}`,
246
- "error",
247
- );
248
- return inspectPiIntercom({ cwd: ctx.cwd, active: false });
249
- } finally {
250
- ctx.ui.setWorkingMessage();
251
- }
165
+ export function buildSetupNoopClosureText() {
166
+ return "No configuration update was confirmed in this setup run. The configuration writer is now hidden. To make a configuration change, run /openpi-setup <request>; do not edit configuration files directly.";
252
167
  }
253
168
 
254
169
  export const CONFIGURE_MY_PI_SETUP_TOOL_NAME = "configure_my_pi_setup";
170
+ const SETUP_REQUEST_CUSTOM_TYPE = "openpi-setup-request";
255
171
 
256
172
  type SetupEpisode = "idle" | "armed" | "active";
257
173
 
@@ -259,6 +175,7 @@ function showConfigureTool(pi: ExtensionAPI) {
259
175
  patchOwnedTools(pi, "setup", {
260
176
  enable: [CONFIGURE_MY_PI_SETUP_TOOL_NAME],
261
177
  });
178
+ return isOwnedToolActive(pi, "setup", CONFIGURE_MY_PI_SETUP_TOOL_NAME);
262
179
  }
263
180
 
264
181
  function hideConfigureTool(pi: ExtensionAPI) {
@@ -269,40 +186,123 @@ function hideConfigureTool(pi: ExtensionAPI) {
269
186
 
270
187
  export default function openPiSetup(pi: ExtensionAPI) {
271
188
  let episode: SetupEpisode = "idle";
189
+ let claimedToolCallId: string | undefined;
190
+ let blockedMatchingClaimCount = 0;
191
+ let requestSequence = 0;
192
+ const pendingRequests: Array<{ requestId: string; prompt: string }> = [];
272
193
  const publishEpisode = () =>
273
194
  pi.events.emit(OPENPI_SETUP_EPISODE_CHANNEL, {
274
- active: episode !== "idle",
195
+ active: episode === "active",
275
196
  } satisfies OpenPiSetupEpisodeState);
276
197
 
277
- const endEpisode = () => {
198
+ const hideActiveWriter = () => {
199
+ claimedToolCallId = undefined;
200
+ blockedMatchingClaimCount = 0;
201
+ episode = pendingRequests.length > 0 ? "armed" : "idle";
202
+ hideConfigureTool(pi);
203
+ publishEpisode();
204
+ };
205
+
206
+ const resetEpisode = () => {
207
+ claimedToolCallId = undefined;
208
+ blockedMatchingClaimCount = 0;
209
+ pendingRequests.length = 0;
278
210
  episode = "idle";
279
211
  hideConfigureTool(pi);
280
212
  publishEpisode();
281
213
  };
282
214
 
283
215
  pi.on("session_start", () => {
284
- endEpisode();
216
+ resetEpisode();
285
217
  });
286
218
 
287
- pi.on("agent_start", () => {
288
- if (episode === "armed") {
289
- episode = "active";
290
- publishEpisode();
219
+ const dispatchNextRequest = (ctx: ExtensionContext) => {
220
+ const request = pendingRequests.shift();
221
+ if (!request) return;
222
+ if (!showConfigureTool(pi)) {
223
+ resetEpisode();
224
+ if (ctx.hasUI) {
225
+ ctx.ui.notify(
226
+ "OpenPI lost ownership of its configuration writer before setup started.",
227
+ "error",
228
+ );
229
+ }
230
+ pi.sendMessage({
231
+ customType: "openpi-setup-closed",
232
+ content:
233
+ "OpenPI could not activate its owned configuration writer, so setup was not started. Check for duplicate or mismatched OpenPI extension sources, then retry with /openpi-setup <request>. Do not edit configuration files directly.",
234
+ display: true,
235
+ details: { reason: "writer_activation_failed" },
236
+ });
237
+ return;
291
238
  }
239
+ claimedToolCallId = undefined;
240
+ blockedMatchingClaimCount = 0;
241
+ episode = "active";
242
+ publishEpisode();
243
+ pi.sendMessage(
244
+ {
245
+ customType: SETUP_REQUEST_CUSTOM_TYPE,
246
+ content: request.prompt,
247
+ display: true,
248
+ details: { requestId: request.requestId },
249
+ },
250
+ { triggerTurn: true },
251
+ );
252
+ };
253
+
254
+ pi.on("tool_call", (event) => {
255
+ if (event.toolName !== CONFIGURE_MY_PI_SETUP_TOOL_NAME) return;
256
+ if (
257
+ episode !== "active" ||
258
+ !isOwnedToolActive(pi, "setup", CONFIGURE_MY_PI_SETUP_TOOL_NAME)
259
+ ) {
260
+ return {
261
+ block: true as const,
262
+ reason:
263
+ "The OpenPI configuration writer is not active for this setup episode. Run /openpi-setup <request> to start a new setup episode.",
264
+ };
265
+ }
266
+ if (claimedToolCallId !== undefined) {
267
+ if (claimedToolCallId === event.toolCallId) {
268
+ blockedMatchingClaimCount += 1;
269
+ }
270
+ return {
271
+ block: true as const,
272
+ reason:
273
+ "This setup episode already admitted one configure_my_pi_setup call. Wait for its result before retrying.",
274
+ };
275
+ }
276
+ claimedToolCallId = event.toolCallId;
292
277
  });
293
278
 
294
279
  pi.on("tool_execution_end", (event) => {
295
280
  if (
296
- episode === "active" &&
297
- event.toolName === CONFIGURE_MY_PI_SETUP_TOOL_NAME &&
298
- !event.isError
299
- ) {
300
- endEpisode();
281
+ episode !== "active" ||
282
+ event.toolName !== CONFIGURE_MY_PI_SETUP_TOOL_NAME ||
283
+ event.toolCallId !== claimedToolCallId
284
+ )
285
+ return;
286
+ if (!event.isError) {
287
+ hideActiveWriter();
288
+ } else if (blockedMatchingClaimCount > 0) {
289
+ blockedMatchingClaimCount -= 1;
290
+ } else {
291
+ claimedToolCallId = undefined;
301
292
  }
302
293
  });
303
294
 
304
- pi.on("agent_settled", () => {
305
- if (episode === "active") endEpisode();
295
+ pi.on("agent_settled", (_event, ctx) => {
296
+ if (episode === "active") {
297
+ hideActiveWriter();
298
+ pi.sendMessage({
299
+ customType: "openpi-setup-closed",
300
+ content: buildSetupNoopClosureText(),
301
+ display: true,
302
+ details: { reason: "settled_without_successful_apply" },
303
+ });
304
+ }
305
+ if (pendingRequests.length > 0) dispatchNextRequest(ctx);
306
306
  });
307
307
 
308
308
  pi.registerTool({
@@ -371,7 +371,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
371
371
  ui_footer_style: Type.Optional(
372
372
  StringEnum(FOOTER_STYLES, {
373
373
  description:
374
- "Footer visual style: plain (Pi theme separators), powerline (ANSI256 colored blocks with  seams), powerline-mono (high-contrast gray powerline). Nerd Font improves separator glyphs only. Omit to preserve the current style (or the preset's style when a preset is applied).",
374
+ "Footer visual style: plain (Pi theme separators), powerline (ANSI256 colored blocks with  seams), powerline-mono (high-contrast gray powerline). A Nerd Font renders Codicon metric glyphs and powerline seams as designed; text stays readable without it. Omit to preserve the current style (or the preset's style when a preset is applied).",
375
375
  }),
376
376
  ),
377
377
  ui_footer_lines: Type.Optional(
@@ -401,13 +401,13 @@ export default function openPiSetup(pi: ExtensionAPI) {
401
401
  bash_tool_display: Type.Optional(
402
402
  StringEnum(DETAIL_DISPLAYS, {
403
403
  description:
404
- "How Bash commands and output render by default: compact keeps a one-line command plus a bounded output preview with a hidden-line count and expands with app.tools.expand; full keeps every command expanded. Omit to preserve the current value.",
404
+ "How Bash commands and output render by default: compact shows one semantic activity row with running/success/failure state; app.tools.expand restores Pi's native command, output, error, timing, and full-output metadata. Full keeps Pi's native rendering expanded by default. Omit to preserve the current value.",
405
405
  }),
406
406
  ),
407
407
  file_mutation_display: Type.Optional(
408
408
  StringEnum(DETAIL_DISPLAYS, {
409
409
  description:
410
- "How Write/Edit content and diffs render by default: compact shows a Claude Code-style folded preview with a hidden-line count and expands with app.tools.expand; full keeps every operation expanded. Omit to preserve the current value.",
410
+ "How Write/Edit content and diffs render by default: compact shows one semantic activity row with path, status, and line/diff counts; app.tools.expand restores Pi's native preview, output, error, and diff. Full keeps Pi's native rendering expanded by default. Omit to preserve the current value.",
411
411
  }),
412
412
  ),
413
413
  subagent_role_models: Type.Optional(SUBAGENT_ROLE_MODELS_SCHEMA),
@@ -415,7 +415,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
415
415
  Type.String({
416
416
  maxLength: POST_EDIT_COMMAND_MAX_CHARS,
417
417
  description:
418
- 'A single shell command (maximum 500 characters) to run in the background after a turn with successful Write/Edit operations, e.g. "npm run format". Runs once per changed turn, not per edit, and only in an interactive TUI session. Set to an empty string to turn it off. Omit to preserve the current value.',
418
+ 'A single shell command (maximum 500 characters) to run in the background after a turn with successful Write/Edit operations, e.g. "npm run format". Set a non-empty command only when the current user\'s /openpi-setup request explicitly asks to configure Post-edit; do not infer one while changing another setting. Runs once per changed turn, not per edit, and only in an interactive TUI session. Set to an empty string to turn it off. Omit to preserve the current value.',
419
419
  }),
420
420
  ),
421
421
  }),
@@ -550,15 +550,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
550
550
 
551
551
  const setupHandler = async (args: string, ctx: ExtensionCommandContext) => {
552
552
  const request = args.trim();
553
- let intercomStatus = inspectPiIntercom({
554
- cwd: ctx.cwd,
555
- active: pi.getAllTools().some(({ name }) => name === "intercom"),
556
- });
557
- intercomStatus = await maybeOfferPiIntercom(ctx, intercomStatus, request);
558
-
559
- const currentConfiguration = formatSetupConfig(loadSetupConfig(), [
560
- formatPiIntercomStatus(intercomStatus),
561
- ]);
553
+ const currentConfiguration = formatSetupConfig(loadSetupConfig());
562
554
  const savedConfigExists = hasSavedSetupConfig();
563
555
  const currentModel = ctx.model
564
556
  ? `${ctx.model.provider}/${ctx.model.id}`
@@ -573,9 +565,9 @@ export default function openPiSetup(pi: ExtensionAPI) {
573
565
  "Current configuration:",
574
566
  currentConfiguration,
575
567
  "",
576
- "Capability discovery is explicit by default; adaptive is an opt-in that keeps only openpi_load_tools visible so the model may load useful groups. Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. Nerd Font only affects powerline separator glyphs. Changes apply immediately in the active TUI session. Intercom installation is handled only by the native setup confirmation; do not install packages or edit its config yourself.",
568
+ "Capability discovery is explicit by default; adaptive is an opt-in that keeps only openpi_load_tools visible so the model may load useful groups. Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. A Nerd Font renders Footer Codicons and powerline seams as designed; text stays readable without it. Changes apply immediately in the active TUI session.",
577
569
  "",
578
- "Use configure_my_pi_setup to apply only the requested OpenPI-owned changes and preserve everything else. Interpret model names from the available Pi registry. Do not edit configuration files directly.",
570
+ "configure_my_pi_setup is available only for this setup run. If the run settles without a successful apply, the writer is hidden and a later change requires /openpi-setup <request>. Use configure_my_pi_setup to apply only the requested OpenPI-owned changes and preserve everything else. Interpret model names from the available Pi registry. Do not edit configuration files directly.",
579
571
  ]
580
572
  : buildInteractiveSetupPrompt({
581
573
  currentConfiguration,
@@ -584,13 +576,17 @@ export default function openPiSetup(pi: ExtensionAPI) {
584
576
  savedConfigExists,
585
577
  });
586
578
 
587
- episode = "armed";
588
- showConfigureTool(pi);
589
- publishEpisode();
590
- pi.sendUserMessage(
591
- prompt.join("\n"),
592
- ctx.isIdle() ? undefined : { deliverAs: "followUp" },
593
- );
579
+ if (!isOwnedToolAvailable(pi, "setup", CONFIGURE_MY_PI_SETUP_TOOL_NAME)) {
580
+ resetEpisode();
581
+ const message =
582
+ "OpenPI could not find its owned configuration writer. Setup was not started; check for duplicate or mismatched OpenPI extension sources.";
583
+ if (ctx.hasUI) ctx.ui.notify(message, "error");
584
+ throw new Error(message);
585
+ }
586
+ const requestId = `setup-${++requestSequence}`;
587
+ pendingRequests.push({ requestId, prompt: prompt.join("\n") });
588
+ if (episode === "idle") episode = "armed";
589
+ if (ctx.isIdle() && episode === "armed") dispatchNextRequest(ctx);
594
590
  };
595
591
 
596
592
  pi.registerCommand("openpi-setup", {
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  type Theme = ExtensionContext["ui"]["theme"];
4
+ type StatusUI = Pick<ExtensionContext["ui"], "setStatus">;
4
5
 
5
6
  export interface ActivityCounts {
6
7
  running: number;
@@ -8,8 +9,6 @@ export interface ActivityCounts {
8
9
  failed: number;
9
10
  }
10
11
 
11
- const SQUARE = "■";
12
-
13
12
  /**
14
13
  * Settled work is an unread notice, not a session tally: `done`/`failed` stay
15
14
  * visible until the user's next explicit request acknowledges them, while
@@ -44,20 +43,52 @@ export function hasActivity(counts: ActivityCounts) {
44
43
  return counts.running + counts.done + counts.failed > 0;
45
44
  }
46
45
 
46
+ /**
47
+ * Pi's `setStatus` requests a global render unconditionally, without diffing
48
+ * the text, so rewriting the same value repaints the whole TUI for nothing.
49
+ * Managers notify on every child event (streaming text, tools, usage), and in
50
+ * the TUI the footer line stays `undefined` throughout, so the vast majority of
51
+ * those writes are no-ops. Keep the last written value and forward only real
52
+ * changes. The `ui` identity is part of the comparison: a new session hands out
53
+ * a new object whose footer starts empty, so the first write there must land.
54
+ */
55
+ export function createStatusWriter(key: string) {
56
+ let lastUI: StatusUI | undefined;
57
+ let lastText: string | undefined;
58
+ return {
59
+ /** Returns whether the write reached Pi. */
60
+ write(ui: StatusUI, text: string | undefined) {
61
+ if (ui === lastUI && text === lastText) return false;
62
+ lastUI = ui;
63
+ lastText = text;
64
+ ui.setStatus(key, text);
65
+ return true;
66
+ },
67
+ /** Forget the cached value so the next write always lands. */
68
+ reset() {
69
+ lastUI = undefined;
70
+ lastText = undefined;
71
+ },
72
+ };
73
+ }
74
+
47
75
  export function formatActivityStatus(
48
76
  theme: Theme,
49
77
  label: "subagents" | "workflows",
50
78
  counts: ActivityCounts,
51
79
  ) {
80
+ // No status glyphs here: the footer line is a static string refreshed on
81
+ // events, so a spinner would freeze between updates — the colored words
82
+ // carry the state on their own.
52
83
  const parts: string[] = [];
53
84
  if (counts.running > 0) {
54
- parts.push(theme.fg("warning", `${SQUARE} ${counts.running} running`));
85
+ parts.push(theme.fg("warning", `${counts.running} running`));
55
86
  }
56
87
  if (counts.done > 0) {
57
- parts.push(theme.fg("success", `${SQUARE} ${counts.done} done`));
88
+ parts.push(theme.fg("success", `${counts.done} done`));
58
89
  }
59
90
  if (counts.failed > 0) {
60
- parts.push(theme.fg("error", `${SQUARE} ${counts.failed} failed`));
91
+ parts.push(theme.fg("error", `${counts.failed} failed`));
61
92
  }
62
93
  parts.push(theme.fg("accent", `/${label}`) + theme.fg("dim", " to view"));
63
94