@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3

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 (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli.js +3621 -3579
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/runtime.d.ts +15 -1
  8. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  9. package/dist/types/advisor/watchdog.d.ts +20 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/config/model-roles.d.ts +1 -1
  12. package/dist/types/config/settings-schema.d.ts +113 -12
  13. package/dist/types/debug/log-viewer.d.ts +1 -0
  14. package/dist/types/debug/raw-sse.d.ts +1 -0
  15. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/edit/index.d.ts +18 -0
  18. package/dist/types/edit/streaming.d.ts +30 -0
  19. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  20. package/dist/types/extensibility/shared-events.d.ts +1 -0
  21. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  22. package/dist/types/extensibility/utils.d.ts +12 -0
  23. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  24. package/dist/types/mcp/transports/index.d.ts +1 -0
  25. package/dist/types/mcp/transports/sse.d.ts +20 -0
  26. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  27. package/dist/types/modes/components/index.d.ts +1 -0
  28. package/dist/types/modes/components/model-selector.d.ts +9 -1
  29. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/component.d.ts +30 -3
  31. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/interactive-mode.d.ts +10 -4
  34. package/dist/types/modes/skill-command.d.ts +32 -0
  35. package/dist/types/modes/types.d.ts +7 -2
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +84 -12
  38. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  39. package/dist/types/session/messages.d.ts +32 -7
  40. package/dist/types/session/messages.test.d.ts +1 -0
  41. package/dist/types/session/session-entries.d.ts +31 -3
  42. package/dist/types/session/session-history-format.d.ts +6 -0
  43. package/dist/types/session/session-loader.d.ts +9 -1
  44. package/dist/types/session/session-manager.d.ts +6 -4
  45. package/dist/types/session/session-storage.d.ts +11 -0
  46. package/dist/types/session/session-title-slot.d.ts +19 -0
  47. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  48. package/dist/types/session/turn-persistence.d.ts +88 -0
  49. package/dist/types/ssh/connection-manager.d.ts +47 -0
  50. package/dist/types/ssh/utils.d.ts +16 -0
  51. package/dist/types/task/executor.d.ts +3 -16
  52. package/dist/types/task/render.d.ts +0 -5
  53. package/dist/types/task/renderer.d.ts +13 -0
  54. package/dist/types/task/types.d.ts +16 -0
  55. package/dist/types/task/yield-assembly.d.ts +28 -0
  56. package/dist/types/tiny/text.d.ts +8 -0
  57. package/dist/types/tools/render-utils.d.ts +2 -0
  58. package/dist/types/tools/review.d.ts +6 -4
  59. package/dist/types/tools/ssh.d.ts +1 -1
  60. package/dist/types/tools/todo.d.ts +6 -0
  61. package/dist/types/tools/yield.d.ts +8 -3
  62. package/dist/types/utils/thinking-display.d.ts +4 -0
  63. package/package.json +12 -12
  64. package/src/advisor/__tests__/advisor.test.ts +438 -10
  65. package/src/advisor/__tests__/config.test.ts +173 -0
  66. package/src/advisor/advise-tool.ts +11 -6
  67. package/src/advisor/config.ts +256 -0
  68. package/src/advisor/index.ts +1 -0
  69. package/src/advisor/runtime.ts +77 -4
  70. package/src/advisor/transcript-recorder.ts +25 -2
  71. package/src/advisor/watchdog.ts +57 -31
  72. package/src/auto-thinking/classifier.ts +2 -2
  73. package/src/autoresearch/index.ts +7 -2
  74. package/src/cli/gc-cli.ts +17 -10
  75. package/src/collab/guest.ts +43 -7
  76. package/src/config/model-registry.ts +80 -18
  77. package/src/config/model-resolver.ts +5 -1
  78. package/src/config/model-roles.ts +3 -3
  79. package/src/config/settings-schema.ts +107 -8
  80. package/src/debug/index.ts +32 -7
  81. package/src/debug/log-viewer.ts +111 -53
  82. package/src/debug/raw-sse.ts +68 -48
  83. package/src/discovery/codex.ts +13 -5
  84. package/src/discovery/omp-extension-roots.ts +38 -13
  85. package/src/edit/hashline/diff.ts +57 -4
  86. package/src/edit/index.ts +21 -0
  87. package/src/edit/streaming.ts +170 -0
  88. package/src/eval/js/shared/local-module-loader.ts +23 -1
  89. package/src/export/html/template.js +13 -7
  90. package/src/extensibility/custom-tools/types.ts +1 -0
  91. package/src/extensibility/extensions/loader.ts +5 -3
  92. package/src/extensibility/extensions/wrapper.ts +9 -3
  93. package/src/extensibility/hooks/loader.ts +3 -3
  94. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  95. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  96. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  97. package/src/extensibility/plugins/manager.ts +76 -5
  98. package/src/extensibility/shared-events.ts +1 -0
  99. package/src/extensibility/tool-event-input.ts +23 -0
  100. package/src/extensibility/utils.ts +74 -0
  101. package/src/internal-urls/docs-index.generated.txt +1 -1
  102. package/src/mcp/client.ts +3 -1
  103. package/src/mcp/manager.ts +12 -5
  104. package/src/mcp/oauth-discovery.ts +5 -29
  105. package/src/mcp/transports/http.ts +3 -1
  106. package/src/mcp/transports/index.ts +1 -0
  107. package/src/mcp/transports/sse.ts +377 -0
  108. package/src/memories/index.ts +15 -6
  109. package/src/mnemopi/backend.ts +2 -2
  110. package/src/modes/acp/acp-agent.ts +1 -1
  111. package/src/modes/components/advisor-config.ts +555 -0
  112. package/src/modes/components/advisor-message.ts +9 -2
  113. package/src/modes/components/agent-hub.ts +9 -4
  114. package/src/modes/components/assistant-message.ts +5 -5
  115. package/src/modes/components/index.ts +2 -0
  116. package/src/modes/components/model-selector.ts +79 -48
  117. package/src/modes/components/settings-selector.ts +1 -0
  118. package/src/modes/components/status-line/component.ts +145 -14
  119. package/src/modes/components/status-line/segments.ts +47 -22
  120. package/src/modes/components/status-line/types.ts +13 -1
  121. package/src/modes/components/tool-execution.ts +47 -6
  122. package/src/modes/controllers/command-controller.ts +23 -2
  123. package/src/modes/controllers/event-controller.ts +114 -11
  124. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  125. package/src/modes/controllers/input-controller.ts +61 -61
  126. package/src/modes/controllers/selector-controller.ts +100 -9
  127. package/src/modes/interactive-mode.ts +65 -10
  128. package/src/modes/print-mode.ts +1 -1
  129. package/src/modes/skill-command.ts +116 -0
  130. package/src/modes/types.ts +7 -2
  131. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  132. package/src/modes/utils/ui-helpers.ts +46 -27
  133. package/src/prompts/agents/reviewer.md +11 -10
  134. package/src/prompts/review-custom-request.md +1 -2
  135. package/src/prompts/review-request.md +1 -2
  136. package/src/prompts/system/interrupted-thinking.md +7 -0
  137. package/src/prompts/system/recap-user.md +9 -0
  138. package/src/prompts/system/subagent-system-prompt.md +8 -5
  139. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  140. package/src/prompts/system/system-prompt.md +0 -1
  141. package/src/prompts/tools/irc.md +2 -2
  142. package/src/prompts/tools/read.md +2 -2
  143. package/src/sdk.ts +40 -50
  144. package/src/session/agent-session.ts +1139 -600
  145. package/src/session/indexed-session-storage.ts +86 -13
  146. package/src/session/messages.test.ts +125 -0
  147. package/src/session/messages.ts +192 -21
  148. package/src/session/redis-session-storage.ts +49 -2
  149. package/src/session/session-entries.ts +39 -2
  150. package/src/session/session-history-format.ts +29 -2
  151. package/src/session/session-listing.ts +54 -24
  152. package/src/session/session-loader.ts +66 -3
  153. package/src/session/session-manager.ts +113 -19
  154. package/src/session/session-persistence.ts +96 -3
  155. package/src/session/session-storage.ts +36 -0
  156. package/src/session/session-title-slot.ts +141 -0
  157. package/src/session/settings-stream-fn.ts +49 -0
  158. package/src/session/sql-session-storage.ts +71 -11
  159. package/src/session/turn-persistence.ts +142 -0
  160. package/src/session/unexpected-stop-classifier.ts +2 -2
  161. package/src/slash-commands/builtin-registry.ts +16 -3
  162. package/src/slash-commands/helpers/mcp.ts +2 -1
  163. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  164. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  165. package/src/ssh/connection-manager.ts +139 -12
  166. package/src/ssh/file-transfer.ts +23 -18
  167. package/src/ssh/ssh-executor.ts +2 -13
  168. package/src/ssh/utils.ts +19 -0
  169. package/src/task/executor.ts +21 -23
  170. package/src/task/render.ts +162 -20
  171. package/src/task/renderer.ts +14 -0
  172. package/src/task/types.ts +17 -0
  173. package/src/task/yield-assembly.ts +207 -0
  174. package/src/tiny/models.ts +8 -6
  175. package/src/tiny/text.ts +37 -7
  176. package/src/tools/ask.ts +55 -4
  177. package/src/tools/image-gen.ts +2 -1
  178. package/src/tools/render-utils.ts +2 -0
  179. package/src/tools/renderers.ts +8 -2
  180. package/src/tools/review.ts +17 -7
  181. package/src/tools/ssh.ts +8 -4
  182. package/src/tools/todo.ts +17 -1
  183. package/src/tools/tts.ts +2 -1
  184. package/src/tools/yield.ts +140 -31
  185. package/src/utils/thinking-display.ts +15 -0
  186. package/src/utils/title-generator.ts +1 -1
  187. package/src/web/search/providers/tavily.ts +36 -19
@@ -4,8 +4,15 @@ import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
4
4
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
5
5
  import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
6
6
  import { Input, Loader, Spacer, setTuiTight, Text } from "@oh-my-pi/pi-tui";
7
- import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
8
- import { formatModelSelectorValue } from "../../config/model-resolver";
7
+ import { getAgentDbPath, getAgentDir, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
8
+ import {
9
+ type AdvisorConfigScope,
10
+ discoverAdvisorConfigs,
11
+ loadWatchdogConfigFile,
12
+ resolveAdvisorConfigEditPath,
13
+ saveWatchdogConfigFile,
14
+ } from "../../advisor";
15
+ import { formatModelSelectorValue, resolveAdvisorRoleSelection } from "../../config/model-resolver";
9
16
  import { getRoleInfo } from "../../config/model-roles";
10
17
  import { settings } from "../../config/settings";
11
18
  import { disableProvider, enableProvider } from "../../discovery";
@@ -49,7 +56,9 @@ import {
49
56
  } from "../../tools";
50
57
  import { shortenPath } from "../../tools/render-utils";
51
58
  import { copyToClipboard } from "../../utils/clipboard";
59
+ import { repo } from "../../utils/git";
52
60
  import { setSessionTerminalTitle } from "../../utils/title-generator";
61
+ import { type AdvisorConfigDeps, AdvisorConfigOverlayComponent } from "../components/advisor-config";
53
62
  import { AgentDashboard } from "../components/agent-dashboard";
54
63
  import { AgentHubOverlayComponent } from "../components/agent-hub";
55
64
  import { AssistantMessageComponent } from "../components/assistant-message";
@@ -163,6 +172,7 @@ export class SelectorController {
163
172
  showHookStatus: settings.get("statusLine.showHookStatus"),
164
173
  sessionAccent: settings.get("statusLine.sessionAccent"),
165
174
  transparent: settings.get("statusLine.transparent"),
175
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
166
176
  ...previewSettings,
167
177
  });
168
178
  this.ctx.updateEditorTopBorder();
@@ -191,6 +201,7 @@ export class SelectorController {
191
201
  showHookStatus: settings.get("statusLine.showHookStatus"),
192
202
  sessionAccent: settings.get("statusLine.sessionAccent"),
193
203
  transparent: settings.get("statusLine.transparent"),
204
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
194
205
  });
195
206
  this.ctx.updateEditorTopBorder();
196
207
  this.ctx.ui.requestRender();
@@ -209,6 +220,78 @@ export class SelectorController {
209
220
  });
210
221
  }
211
222
 
223
+ showAdvisorConfigure(): void {
224
+ const cwd = this.ctx.sessionManager.getCwd();
225
+ const agentDir = getAgentDir() ?? getProjectDir();
226
+ const initialScope: AdvisorConfigScope = "project";
227
+ void (async () => {
228
+ // "Project" scope edits the repo-root WATCHDOG.yml (the project-level file
229
+ // discovery walks), not the launch subdir — `getProjectDir()` is only cwd.
230
+ let projectDir = cwd;
231
+ try {
232
+ projectDir = (await repo.root(cwd)) ?? cwd;
233
+ } catch {
234
+ projectDir = cwd;
235
+ }
236
+ const dirs = { projectDir, agentDir };
237
+ const initialDoc = await loadWatchdogConfigFile(await resolveAdvisorConfigEditPath(initialScope, dirs));
238
+ // Fullscreen editor on the alternate screen (the /settings idiom): the
239
+ // overlay holds the alt buffer + mouse tracking; the transcript stays put.
240
+ let overlayHandle: OverlayHandle | undefined;
241
+ const done = () => {
242
+ overlayHandle?.hide();
243
+ this.focusActiveEditorArea();
244
+ this.ctx.ui.requestRender();
245
+ };
246
+ // Label the seeded implicit-default row with the actual advisor-role model
247
+ // (NOT the first live advisor, which may be a named advisor from another scope).
248
+ const advisorRoleSel = resolveAdvisorRoleSelection(
249
+ this.ctx.settings,
250
+ this.ctx.session.modelRegistry.getAvailable(),
251
+ this.ctx.session.modelRegistry,
252
+ );
253
+ const defaultAdvisorModel = advisorRoleSel?.model;
254
+ const deps: AdvisorConfigDeps = {
255
+ modelRegistry: this.ctx.session.modelRegistry,
256
+ settings: this.ctx.settings,
257
+ scopedModels: this.ctx.session.scopedModels,
258
+ availableToolNames: this.ctx.session.getAdvisorAvailableToolNames(),
259
+ defaultModelLabel: defaultAdvisorModel
260
+ ? `${defaultAdvisorModel.provider}/${defaultAdvisorModel.id}`
261
+ : undefined,
262
+ };
263
+ const overlay = new AdvisorConfigOverlayComponent(this.ctx.ui, deps, initialScope, initialDoc, {
264
+ loadDoc: async scope => loadWatchdogConfigFile(await resolveAdvisorConfigEditPath(scope, dirs)),
265
+ save: async (scope, doc) => {
266
+ await saveWatchdogConfigFile(await resolveAdvisorConfigEditPath(scope, dirs), doc);
267
+ // Re-discover the merged roster (project + user) so the live advisors
268
+ // reflect cross-level precedence, not just the edited file.
269
+ const discovered = await discoverAdvisorConfigs(cwd, agentDir);
270
+ const count = this.ctx.session.applyAdvisorConfigs(discovered.advisors, discovered.sharedInstructions);
271
+ this.ctx.statusLine.invalidate();
272
+ this.ctx.showStatus(
273
+ count > 0
274
+ ? `Saved ${scope} WATCHDOG.yml — ${count} advisor${count === 1 ? "" : "s"} active.`
275
+ : `Saved ${scope} WATCHDOG.yml. Run /advisor on to activate the configured advisors.`,
276
+ );
277
+ this.ctx.ui.requestRender();
278
+ },
279
+ close: done,
280
+ requestRender: () => this.ctx.ui.requestRender(),
281
+ notify: message => this.ctx.showStatus(message),
282
+ });
283
+ overlayHandle = this.ctx.ui.showOverlay(overlay, {
284
+ anchor: "bottom-center",
285
+ width: "100%",
286
+ maxHeight: "100%",
287
+ margin: 0,
288
+ fullscreen: true,
289
+ });
290
+ this.ctx.ui.setFocus(overlay);
291
+ this.ctx.ui.requestRender();
292
+ })();
293
+ }
294
+
212
295
  showHistorySearch(): void {
213
296
  const historyStorage = this.ctx.historyStorage;
214
297
  if (!historyStorage) return;
@@ -451,6 +534,7 @@ export class SelectorController {
451
534
  case "statusLine.showHookStatus":
452
535
  case "statusLine.sessionAccent":
453
536
  case "statusLine.transparent":
537
+ case "statusLine.compactThinkingLevel":
454
538
  case "statusLineSegments":
455
539
  case "statusLineModelThinking":
456
540
  case "statusLinePathAbbreviate":
@@ -471,6 +555,7 @@ export class SelectorController {
471
555
  sessionAccent: settings.get("statusLine.sessionAccent"),
472
556
  transparent: settings.get("statusLine.transparent"),
473
557
  segmentOptions: settings.get("statusLine.segmentOptions"),
558
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
474
559
  };
475
560
  this.ctx.statusLine.updateSettings(statusLineSettings);
476
561
  this.ctx.updateEditorTopBorder();
@@ -536,19 +621,25 @@ export class SelectorController {
536
621
  done();
537
622
  this.ctx.ui.requestRender();
538
623
  } else if (role === "default") {
539
- // Default: update agent state and persist
540
- await this.ctx.session.setModel(model, role, {
624
+ const { switched } = await this.ctx.session.setModel(model, role, {
541
625
  selector,
542
626
  thinkingLevel: concreteThinking,
543
627
  persist: true,
628
+ currentContextTokens,
544
629
  });
545
630
  if (isAuto) {
546
- this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
547
- } else if (concreteThinking && concreteThinking !== ThinkingLevel.Inherit) {
631
+ if (switched) {
632
+ this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
633
+ } else {
634
+ this.ctx.settings.set("defaultThinkingLevel", AUTO_THINKING);
635
+ }
636
+ } else if (switched && concreteThinking && concreteThinking !== ThinkingLevel.Inherit) {
548
637
  this.ctx.session.setThinkingLevel(concreteThinking);
549
638
  }
550
- this.ctx.statusLine.invalidate();
551
- this.ctx.updateEditorBorderColor();
639
+ if (switched) {
640
+ this.ctx.statusLine.invalidate();
641
+ this.ctx.updateEditorBorderColor();
642
+ }
552
643
  this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
553
644
  // Don't call done() - selector stays open for role assignment
554
645
  } else {
@@ -933,7 +1024,7 @@ export class SelectorController {
933
1024
 
934
1025
  this.ctx.clearTransientSessionUi();
935
1026
  this.ctx.statusLine.invalidate();
936
- this.ctx.statusLine.setSessionStartTime(Date.now());
1027
+ this.ctx.statusLine.resetActiveTime();
937
1028
  this.ctx.updateEditorTopBorder();
938
1029
  this.ctx.updateEditorBorderColor();
939
1030
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
@@ -112,6 +112,7 @@ import { renderTreeList } from "../tui/tree-list";
112
112
  import type { EventBus } from "../utils/event-bus";
113
113
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
114
114
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
115
+ import { messageHasDisplayableThinking } from "../utils/thinking-display";
115
116
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
116
117
  import {
117
118
  isSearchProviderId,
@@ -413,14 +414,29 @@ export class InteractiveMode implements InteractiveModeContext {
413
414
  #modelCycleClearTimer: NodeJS.Timeout | undefined;
414
415
  todoPhases: TodoPhase[] = [];
415
416
  hideThinkingBlock = false;
417
+ #sessionsWithDisplayableThinkingContent = new WeakSet<AgentSession>();
418
+ /** Whether the visible session has produced thinking content the user can reveal. */
419
+ get hasDisplayableThinkingContent(): boolean {
420
+ return this.#sessionsWithDisplayableThinkingContent.has(this.viewSession);
421
+ }
422
+ /** Record received reasoning content so Ctrl+T can reveal it even when model metadata says thinking is off. */
423
+ noteDisplayableThinkingContent(message: AgentMessage): boolean {
424
+ if (this.hasDisplayableThinkingContent || !messageHasDisplayableThinking(message, this.proseOnlyThinking)) {
425
+ return false;
426
+ }
427
+ this.#sessionsWithDisplayableThinkingContent.add(this.viewSession);
428
+ return true;
429
+ }
416
430
  /**
417
- * Effective thinking-block visibility: hidden when the user's setting is on
418
- * OR the session thinking level is "off". Some providers (MiniMax, GLM,
419
- * DeepSeek) return thinking blocks even with reasoning disabled; this
420
- * respects the user's intent when they set thinking to "off" (#626).
431
+ * Effective thinking-block visibility: hidden when the user's setting is on,
432
+ * or while thinking is "off" before the session has actually produced
433
+ * displayable thinking content. Some providers return thinking blocks without
434
+ * advertising reasoning support, so observed content unlocks the visibility
435
+ * toggle.
421
436
  */
422
437
  get effectiveHideThinkingBlock(): boolean {
423
- return this.hideThinkingBlock || (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
438
+ const thinkingOff = (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
439
+ return this.hideThinkingBlock || (thinkingOff && !this.hasDisplayableThinkingContent);
424
440
  }
425
441
  proseOnlyThinking = true;
426
442
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
@@ -858,11 +874,6 @@ export class InteractiveMode implements InteractiveModeContext {
858
874
  this.#observerRegistry.subscribeToEventBus(this.#eventBus);
859
875
  }
860
876
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
861
- this.statusLine.setSubagentHubHint(
862
- this.keybindings.getDisplayString("app.agents.hub") ||
863
- this.keybindings.getDisplayString("app.session.observe") ||
864
- undefined,
865
- );
866
877
  this.syncRunningSubagentBadge();
867
878
  this.#observerRegistry.onChange(() => {
868
879
  this.syncRunningSubagentBadge();
@@ -1422,6 +1433,7 @@ export class InteractiveMode implements InteractiveModeContext {
1422
1433
  sessionAccent: settings.get("statusLine.sessionAccent"),
1423
1434
  transparent: settings.get("statusLine.transparent"),
1424
1435
  segmentOptions: settings.get("statusLine.segmentOptions"),
1436
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
1425
1437
  });
1426
1438
  }
1427
1439
 
@@ -1483,11 +1495,47 @@ export class InteractiveMode implements InteractiveModeContext {
1483
1495
  }
1484
1496
 
1485
1497
  rebuildChatFromMessages(): void {
1498
+ // Mid-stream rebuilds (e.g. `/shake`, theme/setting changes that touch the
1499
+ // transcript) replay only committed `state.messages`. The agent's in-flight
1500
+ // `streamMessage` and its still-pending tool calls live OUTSIDE
1501
+ // `state.messages` until `message_end`, so a plain clear+replay detaches
1502
+ // their UI components while keeping the `streamingComponent` / `pendingTools`
1503
+ // references — subsequent `message_update`/`message_end` events would then
1504
+ // update orphaned components that never re-render and the live LLM output
1505
+ // vanishes from the chat (#3656). Snapshot the in-flight components,
1506
+ // clear+replay, then re-append them in their original chat-container order
1507
+ // and restore the `pendingTools` map so streaming routes back into them.
1508
+ const liveComponents: Component[] = [];
1509
+ const livePendingTools = new Map<string, ToolExecutionHandle>();
1510
+ if (this.viewSession?.isStreaming) {
1511
+ const liveSet = new Set<Component>();
1512
+ if (this.streamingComponent) liveSet.add(this.streamingComponent);
1513
+ for (const [id, component] of this.pendingTools) {
1514
+ livePendingTools.set(id, component);
1515
+ liveSet.add(component as unknown as Component);
1516
+ }
1517
+ if (liveSet.size > 0) {
1518
+ for (const child of this.chatContainer.children) {
1519
+ if (liveSet.has(child)) liveComponents.push(child);
1520
+ }
1521
+ }
1522
+ }
1486
1523
  this.chatContainer.clear();
1487
1524
  // Live display uses the compacted transcript tail; export/resume callers
1488
1525
  // can still request the full inline compaction history.
1489
1526
  const context = this.viewSession.buildTranscriptSessionContext({ collapseCompactedHistory: true });
1490
1527
  this.renderSessionContext(context);
1528
+ for (const child of liveComponents) {
1529
+ this.chatContainer.addChild(child);
1530
+ }
1531
+ // `renderSessionContext` clears `pendingTools` at start AND end so the
1532
+ // reconstructed historical tool components don't leak into live tracking.
1533
+ // Restore the in-flight entries afterwards so the next streamed tool-call
1534
+ // delta is routed into the preserved component instead of stacking a
1535
+ // duplicate ToolExecutionComponent below it.
1536
+ for (const [id, component] of livePendingTools) {
1537
+ this.pendingTools.set(id, component);
1538
+ }
1491
1539
  // During the pre-streaming window — after `startPendingSubmission` has
1492
1540
  // optimistically rendered the user's message but before the user
1493
1541
  // `message_start` event lands it in `session` entries — any rebuild
@@ -3588,6 +3636,9 @@ export class InteractiveMode implements InteractiveModeContext {
3588
3636
  sessionContext: SessionContext,
3589
3637
  options?: { updateFooter?: boolean; populateHistory?: boolean },
3590
3638
  ): void {
3639
+ for (const message of sessionContext.messages) {
3640
+ this.noteDisplayableThinkingContent(message);
3641
+ }
3591
3642
  this.#uiHelpers.renderSessionContext(sessionContext, options);
3592
3643
  }
3593
3644
 
@@ -3849,6 +3900,10 @@ export class InteractiveMode implements InteractiveModeContext {
3849
3900
  this.#selectorController.showSettingsSelector();
3850
3901
  }
3851
3902
 
3903
+ showAdvisorConfigure(): void {
3904
+ this.#selectorController.showAdvisorConfigure();
3905
+ }
3906
+
3852
3907
  showHistorySearch(): void {
3853
3908
  this.#selectorController.showHistorySearch();
3854
3909
  }
@@ -83,7 +83,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
83
83
  // Check for error/aborted — skip silent-abort (plan-mode compaction transition)
84
84
  if (
85
85
  (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
86
- !isSilentAbort(assistantMsg.errorMessage)
86
+ !isSilentAbort(assistantMsg)
87
87
  ) {
88
88
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
89
89
  // Flush before this hard exit — it bypasses the awaited postmortem.quit()
@@ -0,0 +1,116 @@
1
+ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
2
+ import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
3
+ import type { InteractiveModeContext } from "./types";
4
+
5
+ type SkillCommandHost = Pick<InteractiveModeContext, "skillCommands" | "session" | "showError">;
6
+
7
+ type SkillPromptMessage = Pick<
8
+ CustomMessage<SkillPromptDetails>,
9
+ "customType" | "content" | "display" | "details" | "attribution"
10
+ > & {
11
+ customType: typeof SKILL_PROMPT_MESSAGE_TYPE;
12
+ content: string | (TextContent | ImageContent)[];
13
+ display: true;
14
+ details: SkillPromptDetails;
15
+ attribution: "user";
16
+ };
17
+
18
+ type SkillPromptOptions = {
19
+ streamingBehavior: "steer" | "followUp";
20
+ queueChipText: string;
21
+ };
22
+
23
+ interface ParsedSkillCommand {
24
+ commandName: string;
25
+ args: string;
26
+ }
27
+
28
+ interface InvokeSkillCommandOptions {
29
+ propagateErrors?: boolean;
30
+ queueOnly?: boolean;
31
+ images?: ImageContent[];
32
+ }
33
+
34
+ /** Built custom-message payload and delivery options for a `/skill:` command. */
35
+ export interface BuiltSkillCommandPrompt {
36
+ message: SkillPromptMessage;
37
+ options: SkillPromptOptions;
38
+ }
39
+
40
+ function parseSkillCommand(text: string): ParsedSkillCommand | undefined {
41
+ if (!text.startsWith("/skill:")) return undefined;
42
+ const spaceIndex = text.indexOf(" ");
43
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
44
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
45
+ return { commandName, args };
46
+ }
47
+
48
+ /** Return true when `text` names a registered `/skill:<name>` command. */
49
+ export function isKnownSkillCommand(ctx: SkillCommandHost, text: string): boolean {
50
+ const parsed = parseSkillCommand(text);
51
+ if (!parsed) return false;
52
+ return ctx.skillCommands.has(parsed.commandName);
53
+ }
54
+
55
+ /** Build the user-attributed custom message for a registered `/skill:<name>` command. */
56
+ export async function buildSkillCommandPrompt(
57
+ ctx: SkillCommandHost,
58
+ text: string,
59
+ streamingBehavior: "steer" | "followUp",
60
+ images?: ImageContent[],
61
+ ): Promise<BuiltSkillCommandPrompt | undefined> {
62
+ const parsed = parseSkillCommand(text);
63
+ if (!parsed) return undefined;
64
+ const skillPath = ctx.skillCommands.get(parsed.commandName);
65
+ if (!skillPath) return undefined;
66
+
67
+ const content = await Bun.file(skillPath).text();
68
+ const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
69
+ const metaLines = [`Skill: ${skillPath}`];
70
+ if (parsed.args) {
71
+ metaLines.push(`User: ${parsed.args}`);
72
+ }
73
+ const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
74
+ const textBlock: TextContent = { type: "text", text: message };
75
+ const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
76
+ const skillName = parsed.commandName.slice("skill:".length);
77
+ const details: SkillPromptDetails = {
78
+ name: skillName || parsed.commandName,
79
+ path: skillPath,
80
+ args: parsed.args || undefined,
81
+ lineCount: body ? body.split("\n").length : 0,
82
+ };
83
+
84
+ return {
85
+ message: {
86
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
87
+ content: promptContent,
88
+ display: true,
89
+ details,
90
+ attribution: "user",
91
+ },
92
+ options: { streamingBehavior, queueChipText: text },
93
+ };
94
+ }
95
+
96
+ /** Invoke a registered `/skill:<name>` command as a user-attributed custom message. */
97
+ export async function invokeSkillCommandFromText(
98
+ ctx: SkillCommandHost,
99
+ text: string,
100
+ streamingBehavior: "steer" | "followUp",
101
+ options?: InvokeSkillCommandOptions,
102
+ ): Promise<boolean> {
103
+ try {
104
+ const built = await buildSkillCommandPrompt(ctx, text, streamingBehavior, options?.images);
105
+ if (!built) return false;
106
+ const promptOptions = options?.queueOnly ? { ...built.options, queueOnly: true } : built.options;
107
+ await ctx.session.promptCustomMessage(built.message, promptOptions);
108
+ return true;
109
+ } catch (err) {
110
+ if (options?.propagateErrors) {
111
+ throw err;
112
+ }
113
+ ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
114
+ return true;
115
+ }
116
+ }
@@ -161,10 +161,14 @@ export interface InteractiveModeContext {
161
161
  hideThinkingBlock: boolean;
162
162
  /**
163
163
  * Effective thinking-block visibility: true when hidden by user setting OR
164
- * thinking level is "off". Read this in render paths instead of
165
- * {@link hideThinkingBlock} so blocks are auto-hidden when thinking is off.
164
+ * thinking level is "off" before the session has produced displayable
165
+ * thinking content.
166
166
  */
167
167
  readonly effectiveHideThinkingBlock: boolean;
168
+ /** Whether this visible session has produced thinking content the user can reveal. */
169
+ readonly hasDisplayableThinkingContent: boolean;
170
+ /** Record a message whose thinking content makes Ctrl+T meaningful even at thinking level "off"; returns true on first observation. */
171
+ noteDisplayableThinkingContent(message: AgentMessage): boolean;
168
172
  proseOnlyThinking: boolean;
169
173
  compactionQueuedMessages: CompactionQueuedMessage[];
170
174
  pendingTools: Map<string, ToolExecutionHandle>;
@@ -342,6 +346,7 @@ export interface InteractiveModeContext {
342
346
 
343
347
  // Selector handling
344
348
  showSettingsSelector(): void;
349
+ showAdvisorConfigure(): void;
345
350
  showHistorySearch(): void;
346
351
  showExtensionsDashboard(): void;
347
352
  showAgentsDashboard(): void;
@@ -146,11 +146,11 @@ export function resolveAssistantErrorMessage(
146
146
  message: AssistantAgentMessage,
147
147
  retryAttempt = 0,
148
148
  ): { hasErrorStop: boolean; errorMessage: string | null } {
149
- const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
149
+ const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message);
150
150
  const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
151
151
  const errorMessage = hasErrorStop
152
152
  ? message.stopReason === "aborted"
153
- ? resolveAbortLabel(message.errorMessage, retryAttempt)
153
+ ? resolveAbortLabel(message, retryAttempt)
154
154
  : message.errorMessage || "Error"
155
155
  : null;
156
156
  return { hasErrorStop, errorMessage };
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message, Usage } from "@oh-my-pi/pi-ai";
3
+ import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
3
4
  import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
4
5
  import type { AdvisorMessageDetails } from "../../advisor";
5
6
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
@@ -44,6 +45,7 @@ import {
44
45
  type SkillPromptDetails,
45
46
  } from "../../session/messages";
46
47
  import type { SessionContext } from "../../session/session-context";
48
+ import { buildSkillCommandPrompt, invokeSkillCommandFromText, isKnownSkillCommand } from "../skill-command";
47
49
  import { createAssistantMessageComponent } from "./interactive-context-helpers";
48
50
  import {
49
51
  assistantHasVisibleContent,
@@ -409,10 +411,10 @@ export class UiHelpers {
409
411
  readGroup?.seal();
410
412
  readGroup = null;
411
413
  const tool = this.ctx.viewSession.getToolByName(content.name);
412
- const renderArgs =
413
- "partialJson" in content
414
- ? { ...content.arguments, __partialJson: content.partialJson }
415
- : content.arguments;
414
+ const partialJson = getStreamingPartialJson(content);
415
+ const renderArgs = partialJson
416
+ ? { ...content.arguments, __partialJson: partialJson }
417
+ : content.arguments;
416
418
  const component = new ToolExecutionComponent(
417
419
  content.name,
418
420
  renderArgs,
@@ -666,6 +668,15 @@ export class UiHelpers {
666
668
  }
667
669
 
668
670
  async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
671
+ if (
672
+ await invokeSkillCommandFromText(this.ctx, message.text, message.mode, {
673
+ propagateErrors: true,
674
+ queueOnly: true,
675
+ images: message.images,
676
+ })
677
+ ) {
678
+ return;
679
+ }
669
680
  if (this.ctx.isKnownSlashCommand(message.text)) {
670
681
  await this.ctx.session.prompt(message.text);
671
682
  return;
@@ -753,29 +764,37 @@ export class UiHelpers {
753
764
  await this.#deliverQueuedMessage(message);
754
765
  }
755
766
 
756
- // Pass streamingBehavior so that if the session is still streaming when
757
- // compaction-end fires (race window between isStreaming flipping false and
758
- // the event landing here), prompt() routes the message into the steer/
759
- // follow-up queue instead of throwing AgentBusyError. When the session is
760
- // genuinely idle, streamingBehavior is ignored and a fresh prompt runs as
761
- // before. This keeps the steer preview honest: if delivery has to be
762
- // deferred, the message lands in the same queue every other consumer
763
- // (Alt+Up dequeue, post-stream drain) already drains, instead of being
764
- // stranded in compactionQueuedMessages with no drainer.
765
- //
766
- // firstPrompt is fire-and-forget — its rejection is funneled through
767
- // `restoreQueue` rather than rethrown, so we use the primitive
768
- // recordLocalSubmission and dispose manually in the catch.
769
- const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text, firstPrompt.images?.length ?? 0);
770
- const promptPromise = this.ctx.session
771
- .prompt(firstPrompt.text, {
772
- streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
773
- images: firstPrompt.images,
774
- })
775
- .catch((error: unknown) => {
776
- disposeFirstPrompt();
777
- restoreQueue(error);
778
- });
767
+ // First prompt is fire-and-forget its rejection is funneled through
768
+ // `restoreQueue` rather than rethrown. Plain prompts use primitive
769
+ // recordLocalSubmission and dispose manually in the catch. Skill prompts
770
+ // are rebuilt as user-attributed custom messages so queued `/skill:` text
771
+ // is not sent as a literal prompt after compaction.
772
+ let promptPromise: Promise<unknown>;
773
+ if (isKnownSkillCommand(this.ctx, firstPrompt.text)) {
774
+ const built = await buildSkillCommandPrompt(
775
+ this.ctx,
776
+ firstPrompt.text,
777
+ firstPrompt.mode,
778
+ firstPrompt.images,
779
+ );
780
+ promptPromise = built
781
+ ? this.ctx.session.promptCustomMessage(built.message, built.options).catch(restoreQueue)
782
+ : Promise.resolve();
783
+ } else {
784
+ const disposeFirstPrompt = this.ctx.recordLocalSubmission(
785
+ firstPrompt.text,
786
+ firstPrompt.images?.length ?? 0,
787
+ );
788
+ promptPromise = this.ctx.session
789
+ .prompt(firstPrompt.text, {
790
+ streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
791
+ images: firstPrompt.images,
792
+ })
793
+ .catch((error: unknown) => {
794
+ disposeFirstPrompt();
795
+ restoreQueue(error);
796
+ });
797
+ }
779
798
 
780
799
  for (const message of rest) {
781
800
  await this.#deliverQueuedMessage(message);
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: reviewer
3
3
  description: "Code review specialist for quality/security analysis"
4
- tools: read, grep, glob, bash, lsp, web_search, ast_grep, report_finding
4
+ tools: read, grep, glob, bash, lsp, web_search, ast_grep
5
5
  spawns: explore
6
6
  model: pi/slow
7
7
  thinking-level: high
@@ -22,7 +22,7 @@ output:
22
22
  optionalProperties:
23
23
  findings:
24
24
  metadata:
25
- description: Auto-populated from report_finding; don't set manually
25
+ description: "Populate via incremental yield sections under type: [\"findings\"]; don't repeat it in a final payload."
26
26
  elements:
27
27
  properties:
28
28
  title:
@@ -60,8 +60,8 @@ Identify bugs the author would want fixed before merge.
60
60
  <procedure>
61
61
  1. Run `git diff`, `jj diff --git`, or `gh pr diff <number>` to view patch
62
62
  2. Read modified files for full context
63
- 3. Call `report_finding` per issue
64
- 4. Call `yield` with verdict
63
+ 3. Record each issue with incremental `yield` using `type: ["findings"]`
64
+ 4. Record `overall_correctness`, `explanation`, and `confidence` with incremental `yield` sections, then stop so idle finalization assembles the result
65
65
 
66
66
  Bash is read-only: `git diff`, `git log`, `git show`, `jj diff --git`, `gh pr diff`. You NEVER make file edits or trigger builds.
67
67
  </procedure>
@@ -115,7 +115,7 @@ memcpy(buf, data.ptr, data.length);
115
115
  </example>
116
116
 
117
117
  <output>
118
- Each `report_finding` requires:
118
+ Each finding uses incremental `yield` with `type: ["findings"]` and `result.data` containing:
119
119
  - `title`: Imperative, ≤80 chars
120
120
  - `body`: One paragraph
121
121
  - `priority`: 0-3
@@ -123,11 +123,12 @@ Each `report_finding` requires:
123
123
  - `file_path`: Path to affected file
124
124
  - `line_start`, `line_end`: Range ≤10 lines, must overlap diff
125
125
 
126
- Final `yield` call (payload under `result.data`):
127
- - `result.data.overall_correctness`: "correct" (no bugs/blockers) or "incorrect"
128
- - `result.data.explanation`: Plain text, 1-3 sentences summarizing verdict. Don't repeat findings (captured via `report_finding`).
129
- - `result.data.confidence`: 0.0-1.0
130
- - `result.data.findings`: Optional; MUST omit (auto-populated from `report_finding`)
126
+ Verdict fields also use incremental `yield` sections:
127
+ - `type: ["overall_correctness"]` with `"correct"` (no bugs/blockers) or `"incorrect"`
128
+ - `type: ["explanation"]` with a plain-text 1-3 sentence verdict summary
129
+ - `type: ["confidence"]` with a 0.0-1.0 confidence value
130
+
131
+ Do not emit a separate submit tool call or duplicate `findings` in another payload. Once all sections are recorded, stop and let idle finalization assemble the result.
131
132
 
132
133
  You NEVER output JSON or code blocks.
133
134
 
@@ -14,8 +14,7 @@ Create exactly **1 reviewer task**. Its assignment MUST include the custom instr
14
14
  Reviewer MUST:
15
15
  1. Follow the custom instructions below
16
16
  2. Read the referenced files or workspace context needed to evaluate them
17
- 3. Call `report_finding` per issue
18
- 4. Call `yield` with verdict when done
17
+ 3. Use incremental `yield` sections for findings and verdict fields; do NOT call a separate finding tool
19
18
 
20
19
  ### Custom Instructions
21
20
 
@@ -38,8 +38,7 @@ Reviewer MUST:
38
38
  1. Focus ONLY on assigned files
39
39
  2. {{#if skipDiff}}{{diffInstruction}}{{else}}MUST use diff hunks below (NEVER re-run git diff){{/if}}
40
40
  3. {{contextInstruction}}
41
- 4. Call `report_finding` per issue
42
- 5. Call `yield` with verdict when done
41
+ 4. Use incremental `yield` sections for findings and verdict fields; do NOT call a separate finding tool
43
42
 
44
43
  {{#if skipDiff}}
45
44
  ### Diff Previews