@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4

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 (164) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3682 -3615
  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/transcript-recorder.d.ts +13 -2
  8. package/dist/types/advisor/watchdog.d.ts +20 -0
  9. package/dist/types/cli/update-cli.d.ts +15 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/collab/replication-shrink.d.ts +39 -0
  12. package/dist/types/config/provider-globals.d.ts +7 -0
  13. package/dist/types/config/settings-schema.d.ts +81 -0
  14. package/dist/types/debug/log-viewer.d.ts +1 -0
  15. package/dist/types/debug/raw-sse.d.ts +1 -0
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  18. package/dist/types/extensibility/utils.d.ts +12 -0
  19. package/dist/types/mcp/transports/index.d.ts +1 -0
  20. package/dist/types/mcp/transports/sse.d.ts +20 -0
  21. package/dist/types/memories/index.d.ts +20 -1
  22. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  23. package/dist/types/modes/components/index.d.ts +1 -0
  24. package/dist/types/modes/components/model-selector.d.ts +9 -1
  25. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  26. package/dist/types/modes/components/status-line/component.d.ts +35 -1
  27. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  28. package/dist/types/modes/controllers/command-controller.d.ts +3 -3
  29. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  30. package/dist/types/modes/interactive-mode.d.ts +11 -4
  31. package/dist/types/modes/skill-command.d.ts +32 -0
  32. package/dist/types/modes/types.d.ts +9 -3
  33. package/dist/types/session/agent-session.d.ts +58 -10
  34. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  35. package/dist/types/session/messages.d.ts +26 -0
  36. package/dist/types/session/messages.test.d.ts +1 -0
  37. package/dist/types/session/session-entries.d.ts +31 -3
  38. package/dist/types/session/session-history-format.d.ts +6 -0
  39. package/dist/types/session/session-loader.d.ts +9 -1
  40. package/dist/types/session/session-manager.d.ts +8 -7
  41. package/dist/types/session/session-storage.d.ts +11 -0
  42. package/dist/types/session/session-title-slot.d.ts +19 -0
  43. package/dist/types/ssh/connection-manager.d.ts +47 -0
  44. package/dist/types/ssh/utils.d.ts +16 -0
  45. package/dist/types/task/executor.d.ts +3 -16
  46. package/dist/types/task/render.d.ts +0 -5
  47. package/dist/types/task/renderer.d.ts +13 -0
  48. package/dist/types/task/types.d.ts +16 -0
  49. package/dist/types/task/yield-assembly.d.ts +28 -0
  50. package/dist/types/tiny/text.d.ts +8 -0
  51. package/dist/types/tools/render-utils.d.ts +2 -0
  52. package/dist/types/tools/review.d.ts +6 -4
  53. package/dist/types/tools/ssh.d.ts +1 -1
  54. package/dist/types/tools/todo.d.ts +6 -0
  55. package/dist/types/tools/yield.d.ts +8 -3
  56. package/dist/types/utils/thinking-display.d.ts +4 -0
  57. package/package.json +12 -12
  58. package/src/advisor/__tests__/advisor.test.ts +242 -10
  59. package/src/advisor/__tests__/config.test.ts +173 -0
  60. package/src/advisor/advise-tool.ts +11 -6
  61. package/src/advisor/config.ts +256 -0
  62. package/src/advisor/index.ts +1 -0
  63. package/src/advisor/runtime.ts +12 -2
  64. package/src/advisor/transcript-recorder.ts +25 -2
  65. package/src/advisor/watchdog.ts +57 -31
  66. package/src/autolearn/controller.ts +13 -22
  67. package/src/autoresearch/index.ts +7 -2
  68. package/src/cli/gc-cli.ts +17 -10
  69. package/src/cli/update-cli.ts +254 -0
  70. package/src/collab/guest.ts +43 -7
  71. package/src/collab/host.ts +13 -8
  72. package/src/collab/replication-shrink.ts +111 -0
  73. package/src/config/model-registry.ts +80 -18
  74. package/src/config/provider-globals.ts +25 -0
  75. package/src/config/settings-schema.ts +77 -0
  76. package/src/debug/index.ts +32 -7
  77. package/src/debug/log-viewer.ts +111 -53
  78. package/src/debug/raw-sse.ts +68 -48
  79. package/src/discovery/codex.ts +13 -5
  80. package/src/edit/hashline/diff.ts +57 -4
  81. package/src/eval/__tests__/julia-prelude.test.ts +2 -2
  82. package/src/eval/js/shared/local-module-loader.ts +23 -1
  83. package/src/export/html/template.js +13 -7
  84. package/src/extensibility/extensions/loader.ts +5 -3
  85. package/src/extensibility/extensions/wrapper.ts +9 -3
  86. package/src/extensibility/hooks/loader.ts +3 -3
  87. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  88. package/src/extensibility/plugins/manager.ts +2 -1
  89. package/src/extensibility/tool-event-input.ts +23 -0
  90. package/src/extensibility/utils.ts +74 -0
  91. package/src/internal-urls/docs-index.generated.txt +1 -1
  92. package/src/mcp/client.ts +3 -1
  93. package/src/mcp/manager.ts +12 -5
  94. package/src/mcp/transports/index.ts +1 -0
  95. package/src/mcp/transports/sse.ts +377 -0
  96. package/src/memories/index.ts +130 -15
  97. package/src/memory-backend/local-backend.ts +5 -3
  98. package/src/modes/components/advisor-config.ts +555 -0
  99. package/src/modes/components/advisor-message.ts +9 -2
  100. package/src/modes/components/agent-hub.ts +9 -4
  101. package/src/modes/components/index.ts +2 -0
  102. package/src/modes/components/model-selector.ts +79 -48
  103. package/src/modes/components/settings-selector.ts +1 -0
  104. package/src/modes/components/status-line/component.ts +150 -5
  105. package/src/modes/components/status-line/segments.ts +46 -21
  106. package/src/modes/components/status-line/types.ts +13 -1
  107. package/src/modes/components/tool-execution.ts +47 -6
  108. package/src/modes/controllers/command-controller.ts +27 -36
  109. package/src/modes/controllers/event-controller.ts +113 -1
  110. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  111. package/src/modes/controllers/input-controller.ts +61 -61
  112. package/src/modes/controllers/selector-controller.ts +100 -9
  113. package/src/modes/interactive-mode.ts +74 -25
  114. package/src/modes/skill-command.ts +116 -0
  115. package/src/modes/types.ts +9 -3
  116. package/src/modes/utils/ui-helpers.ts +41 -23
  117. package/src/prompts/agents/reviewer.md +11 -10
  118. package/src/prompts/goals/goal-todo-context.md +12 -0
  119. package/src/prompts/review-custom-request.md +1 -2
  120. package/src/prompts/review-request.md +1 -2
  121. package/src/prompts/system/interrupted-thinking.md +7 -0
  122. package/src/prompts/system/recap-user.md +9 -0
  123. package/src/prompts/system/subagent-system-prompt.md +8 -5
  124. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  125. package/src/prompts/system/system-prompt.md +0 -1
  126. package/src/prompts/tools/irc.md +2 -2
  127. package/src/prompts/tools/read.md +2 -2
  128. package/src/sdk.ts +28 -24
  129. package/src/session/agent-session.ts +899 -429
  130. package/src/session/indexed-session-storage.ts +86 -13
  131. package/src/session/messages.test.ts +125 -0
  132. package/src/session/messages.ts +172 -9
  133. package/src/session/redis-session-storage.ts +49 -2
  134. package/src/session/session-entries.ts +39 -2
  135. package/src/session/session-history-format.ts +29 -2
  136. package/src/session/session-listing.ts +54 -24
  137. package/src/session/session-loader.ts +66 -3
  138. package/src/session/session-manager.ts +115 -22
  139. package/src/session/session-persistence.ts +95 -1
  140. package/src/session/session-storage.ts +36 -0
  141. package/src/session/session-title-slot.ts +141 -0
  142. package/src/session/sql-session-storage.ts +71 -11
  143. package/src/slash-commands/builtin-registry.ts +23 -24
  144. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  145. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  146. package/src/ssh/connection-manager.ts +139 -12
  147. package/src/ssh/file-transfer.ts +23 -18
  148. package/src/ssh/ssh-executor.ts +2 -13
  149. package/src/ssh/utils.ts +19 -0
  150. package/src/task/executor.ts +21 -23
  151. package/src/task/render.ts +162 -20
  152. package/src/task/renderer.ts +14 -0
  153. package/src/task/types.ts +17 -0
  154. package/src/task/yield-assembly.ts +207 -0
  155. package/src/tiny/text.ts +23 -0
  156. package/src/tools/ask.ts +55 -4
  157. package/src/tools/render-utils.ts +2 -0
  158. package/src/tools/renderers.ts +8 -2
  159. package/src/tools/review.ts +17 -7
  160. package/src/tools/ssh.ts +8 -4
  161. package/src/tools/todo.ts +17 -1
  162. package/src/tools/yield.ts +140 -31
  163. package/src/utils/thinking-display.ts +15 -0
  164. package/src/prompts/system/autolearn-nudge.md +0 -5
@@ -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 });
@@ -53,6 +53,7 @@ import { reset as resetCapabilities } from "../capability";
53
53
  import type { CollabGuestLink } from "../collab/guest";
54
54
  import type { CollabHost } from "../collab/host";
55
55
  import { KeybindingsManager } from "../config/keybindings";
56
+ import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
56
57
  import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
57
58
  import { clearClaudePluginRootsCache } from "../discovery/helpers";
58
59
  import type {
@@ -100,7 +101,6 @@ import { STTController, type SttState } from "../stt";
100
101
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
101
102
  import { formatTaskId } from "../task/render";
102
103
  import type { LspStartupServerInfo } from "../tools";
103
- import { isImageProviderPreference, setPreferredImageProvider } from "../tools/image-gen";
104
104
  import { normalizeLocalScheme } from "../tools/path-utils";
105
105
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
106
106
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
@@ -112,13 +112,8 @@ 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
- import {
117
- isSearchProviderId,
118
- isSearchProviderPreference,
119
- setExcludedSearchProviders,
120
- setPreferredSearchProvider,
121
- } from "../web/search";
122
117
  import type { AssistantMessageComponent } from "./components/assistant-message";
123
118
  import type { BashExecutionComponent } from "./components/bash-execution";
124
119
  import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
@@ -383,6 +378,7 @@ export class InteractiveMode implements InteractiveModeContext {
383
378
  chatContainer: TranscriptContainer;
384
379
  pendingMessagesContainer: Container;
385
380
  statusContainer: Container;
381
+ todoReminderContainer: Container;
386
382
  todoContainer: Container;
387
383
  subagentContainer: Container;
388
384
  btwContainer: Container;
@@ -413,14 +409,29 @@ export class InteractiveMode implements InteractiveModeContext {
413
409
  #modelCycleClearTimer: NodeJS.Timeout | undefined;
414
410
  todoPhases: TodoPhase[] = [];
415
411
  hideThinkingBlock = false;
412
+ #sessionsWithDisplayableThinkingContent = new WeakSet<AgentSession>();
413
+ /** Whether the visible session has produced thinking content the user can reveal. */
414
+ get hasDisplayableThinkingContent(): boolean {
415
+ return this.#sessionsWithDisplayableThinkingContent.has(this.viewSession);
416
+ }
417
+ /** Record received reasoning content so Ctrl+T can reveal it even when model metadata says thinking is off. */
418
+ noteDisplayableThinkingContent(message: AgentMessage): boolean {
419
+ if (this.hasDisplayableThinkingContent || !messageHasDisplayableThinking(message, this.proseOnlyThinking)) {
420
+ return false;
421
+ }
422
+ this.#sessionsWithDisplayableThinkingContent.add(this.viewSession);
423
+ return true;
424
+ }
416
425
  /**
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).
426
+ * Effective thinking-block visibility: hidden when the user's setting is on,
427
+ * or while thinking is "off" before the session has actually produced
428
+ * displayable thinking content. Some providers return thinking blocks without
429
+ * advertising reasoning support, so observed content unlocks the visibility
430
+ * toggle.
421
431
  */
422
432
  get effectiveHideThinkingBlock(): boolean {
423
- return this.hideThinkingBlock || (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
433
+ const thinkingOff = (this.viewSession?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
434
+ return this.hideThinkingBlock || (thinkingOff && !this.hasDisplayableThinkingContent);
424
435
  }
425
436
  proseOnlyThinking = true;
426
437
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
@@ -536,6 +547,7 @@ export class InteractiveMode implements InteractiveModeContext {
536
547
  this.retryLoader = undefined;
537
548
  }
538
549
  this.statusContainer.clear();
550
+ this.todoReminderContainer.clear();
539
551
  this.pendingMessagesContainer.clear();
540
552
  this.#cancelModelCycleClearTimer();
541
553
  this.modelCycleContainer.clear();
@@ -615,6 +627,7 @@ export class InteractiveMode implements InteractiveModeContext {
615
627
  this.chatContainer = new TranscriptContainer();
616
628
  this.pendingMessagesContainer = new Container();
617
629
  this.statusContainer = new AnchoredLiveContainer();
630
+ this.todoReminderContainer = new AnchoredLiveContainer();
618
631
  this.todoContainer = new AnchoredLiveContainer();
619
632
  this.subagentContainer = new AnchoredLiveContainer();
620
633
  this.btwContainer = new AnchoredLiveContainer();
@@ -834,6 +847,7 @@ export class InteractiveMode implements InteractiveModeContext {
834
847
 
835
848
  this.ui.addChild(this.chatContainer);
836
849
  this.ui.addChild(this.pendingMessagesContainer);
850
+ this.ui.addChild(this.todoReminderContainer);
837
851
  this.ui.addChild(this.todoContainer);
838
852
  this.ui.addChild(this.subagentContainer);
839
853
  this.ui.addChild(this.btwContainer);
@@ -1037,18 +1051,7 @@ export class InteractiveMode implements InteractiveModeContext {
1037
1051
  // module-level search/image provider state reflects the destination
1038
1052
  // project's configuration. Without this, the previous project's
1039
1053
  // exclusions leak and newly-excluded providers are still used.
1040
- const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
1041
- if (Array.isArray(excludedWebSearchProviders)) {
1042
- setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
1043
- }
1044
- const webSearchProvider = settings.get("providers.webSearch");
1045
- if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
1046
- setPreferredSearchProvider(webSearchProvider);
1047
- }
1048
- const imageProvider = settings.get("providers.image");
1049
- if (isImageProviderPreference(imageProvider)) {
1050
- setPreferredImageProvider(imageProvider);
1051
- }
1054
+ applyProviderGlobalsFromSettings(settings);
1052
1055
  }
1053
1056
  // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
1054
1057
  // the next prompt sees everything scoped to the new project directory.
@@ -1417,6 +1420,7 @@ export class InteractiveMode implements InteractiveModeContext {
1417
1420
  sessionAccent: settings.get("statusLine.sessionAccent"),
1418
1421
  transparent: settings.get("statusLine.transparent"),
1419
1422
  segmentOptions: settings.get("statusLine.segmentOptions"),
1423
+ compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
1420
1424
  });
1421
1425
  }
1422
1426
 
@@ -1478,11 +1482,47 @@ export class InteractiveMode implements InteractiveModeContext {
1478
1482
  }
1479
1483
 
1480
1484
  rebuildChatFromMessages(): void {
1485
+ // Mid-stream rebuilds (e.g. `/shake`, theme/setting changes that touch the
1486
+ // transcript) replay only committed `state.messages`. The agent's in-flight
1487
+ // `streamMessage` and its still-pending tool calls live OUTSIDE
1488
+ // `state.messages` until `message_end`, so a plain clear+replay detaches
1489
+ // their UI components while keeping the `streamingComponent` / `pendingTools`
1490
+ // references — subsequent `message_update`/`message_end` events would then
1491
+ // update orphaned components that never re-render and the live LLM output
1492
+ // vanishes from the chat (#3656). Snapshot the in-flight components,
1493
+ // clear+replay, then re-append them in their original chat-container order
1494
+ // and restore the `pendingTools` map so streaming routes back into them.
1495
+ const liveComponents: Component[] = [];
1496
+ const livePendingTools = new Map<string, ToolExecutionHandle>();
1497
+ if (this.viewSession?.isStreaming) {
1498
+ const liveSet = new Set<Component>();
1499
+ if (this.streamingComponent) liveSet.add(this.streamingComponent);
1500
+ for (const [id, component] of this.pendingTools) {
1501
+ livePendingTools.set(id, component);
1502
+ liveSet.add(component as unknown as Component);
1503
+ }
1504
+ if (liveSet.size > 0) {
1505
+ for (const child of this.chatContainer.children) {
1506
+ if (liveSet.has(child)) liveComponents.push(child);
1507
+ }
1508
+ }
1509
+ }
1481
1510
  this.chatContainer.clear();
1482
1511
  // Live display uses the compacted transcript tail; export/resume callers
1483
1512
  // can still request the full inline compaction history.
1484
1513
  const context = this.viewSession.buildTranscriptSessionContext({ collapseCompactedHistory: true });
1485
1514
  this.renderSessionContext(context);
1515
+ for (const child of liveComponents) {
1516
+ this.chatContainer.addChild(child);
1517
+ }
1518
+ // `renderSessionContext` clears `pendingTools` at start AND end so the
1519
+ // reconstructed historical tool components don't leak into live tracking.
1520
+ // Restore the in-flight entries afterwards so the next streamed tool-call
1521
+ // delta is routed into the preserved component instead of stacking a
1522
+ // duplicate ToolExecutionComponent below it.
1523
+ for (const [id, component] of livePendingTools) {
1524
+ this.pendingTools.set(id, component);
1525
+ }
1486
1526
  // During the pre-streaming window — after `startPendingSubmission` has
1487
1527
  // optimistically rendered the user's message but before the user
1488
1528
  // `message_start` event lands it in `session` entries — any rebuild
@@ -1571,8 +1611,8 @@ export class InteractiveMode implements InteractiveModeContext {
1571
1611
  }),
1572
1612
  }));
1573
1613
  if (!mutated) return;
1574
- this.todoPhases = next;
1575
1614
  this.session.setTodoPhases(next);
1615
+ this.setTodos(next);
1576
1616
  }
1577
1617
 
1578
1618
  #cancelTodoAutoClearTimer(): void {
@@ -3583,6 +3623,9 @@ export class InteractiveMode implements InteractiveModeContext {
3583
3623
  sessionContext: SessionContext,
3584
3624
  options?: { updateFooter?: boolean; populateHistory?: boolean },
3585
3625
  ): void {
3626
+ for (const message of sessionContext.messages) {
3627
+ this.noteDisplayableThinkingContent(message);
3628
+ }
3586
3629
  this.#uiHelpers.renderSessionContext(sessionContext, options);
3587
3630
  }
3588
3631
 
@@ -3844,6 +3887,10 @@ export class InteractiveMode implements InteractiveModeContext {
3844
3887
  this.#selectorController.showSettingsSelector();
3845
3888
  }
3846
3889
 
3890
+ showAdvisorConfigure(): void {
3891
+ this.#selectorController.showAdvisorConfigure();
3892
+ }
3893
+
3847
3894
  showHistorySearch(): void {
3848
3895
  this.#selectorController.showHistorySearch();
3849
3896
  }
@@ -4029,12 +4076,14 @@ export class InteractiveMode implements InteractiveModeContext {
4029
4076
  },
4030
4077
  ];
4031
4078
  }
4079
+ this.todoReminderContainer.clear();
4032
4080
  this.#syncTodoAutoClearTimer();
4033
4081
  this.#renderTodoList();
4034
4082
  this.ui.requestRender();
4035
4083
  }
4036
4084
 
4037
4085
  async reloadTodos(): Promise<void> {
4086
+ this.todoReminderContainer.clear();
4038
4087
  await this.#loadTodoList();
4039
4088
  this.ui.requestRender();
4040
4089
  }
@@ -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
+ }
@@ -96,6 +96,7 @@ export interface InteractiveModeContext {
96
96
  chatContainer: TranscriptContainer;
97
97
  pendingMessagesContainer: Container;
98
98
  statusContainer: Container;
99
+ todoReminderContainer: Container;
99
100
  todoContainer: Container;
100
101
  subagentContainer: Container;
101
102
  btwContainer: Container;
@@ -121,7 +122,7 @@ export interface InteractiveModeContext {
121
122
  focusParentSession(): Promise<void>;
122
123
  /** Return the view to the main session (delegates to SessionFocusController.unfocus). */
123
124
  unfocusSession(): Promise<void>;
124
- /** Clear loader, status/pending containers, streaming state, and pending tools. */
125
+ /** Clear loader, transient HUD/pending containers, streaming state, and pending tools. */
125
126
  clearTransientSessionUi(): void;
126
127
  settings: Settings;
127
128
  keybindings: KeybindingsManager;
@@ -161,10 +162,14 @@ export interface InteractiveModeContext {
161
162
  hideThinkingBlock: boolean;
162
163
  /**
163
164
  * 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.
165
+ * thinking level is "off" before the session has produced displayable
166
+ * thinking content.
166
167
  */
167
168
  readonly effectiveHideThinkingBlock: boolean;
169
+ /** Whether this visible session has produced thinking content the user can reveal. */
170
+ readonly hasDisplayableThinkingContent: boolean;
171
+ /** Record a message whose thinking content makes Ctrl+T meaningful even at thinking level "off"; returns true on first observation. */
172
+ noteDisplayableThinkingContent(message: AgentMessage): boolean;
168
173
  proseOnlyThinking: boolean;
169
174
  compactionQueuedMessages: CompactionQueuedMessage[];
170
175
  pendingTools: Map<string, ToolExecutionHandle>;
@@ -342,6 +347,7 @@ export interface InteractiveModeContext {
342
347
 
343
348
  // Selector handling
344
349
  showSettingsSelector(): void;
350
+ showAdvisorConfigure(): void;
345
351
  showHistorySearch(): void;
346
352
  showExtensionsDashboard(): void;
347
353
  showAgentsDashboard(): void;
@@ -45,6 +45,7 @@ import {
45
45
  type SkillPromptDetails,
46
46
  } from "../../session/messages";
47
47
  import type { SessionContext } from "../../session/session-context";
48
+ import { buildSkillCommandPrompt, invokeSkillCommandFromText, isKnownSkillCommand } from "../skill-command";
48
49
  import { createAssistantMessageComponent } from "./interactive-context-helpers";
49
50
  import {
50
51
  assistantHasVisibleContent,
@@ -667,6 +668,15 @@ export class UiHelpers {
667
668
  }
668
669
 
669
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
+ }
670
680
  if (this.ctx.isKnownSlashCommand(message.text)) {
671
681
  await this.ctx.session.prompt(message.text);
672
682
  return;
@@ -754,29 +764,37 @@ export class UiHelpers {
754
764
  await this.#deliverQueuedMessage(message);
755
765
  }
756
766
 
757
- // Pass streamingBehavior so that if the session is still streaming when
758
- // compaction-end fires (race window between isStreaming flipping false and
759
- // the event landing here), prompt() routes the message into the steer/
760
- // follow-up queue instead of throwing AgentBusyError. When the session is
761
- // genuinely idle, streamingBehavior is ignored and a fresh prompt runs as
762
- // before. This keeps the steer preview honest: if delivery has to be
763
- // deferred, the message lands in the same queue every other consumer
764
- // (Alt+Up dequeue, post-stream drain) already drains, instead of being
765
- // stranded in compactionQueuedMessages with no drainer.
766
- //
767
- // firstPrompt is fire-and-forget — its rejection is funneled through
768
- // `restoreQueue` rather than rethrown, so we use the primitive
769
- // recordLocalSubmission and dispose manually in the catch.
770
- const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text, firstPrompt.images?.length ?? 0);
771
- const promptPromise = this.ctx.session
772
- .prompt(firstPrompt.text, {
773
- streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
774
- images: firstPrompt.images,
775
- })
776
- .catch((error: unknown) => {
777
- disposeFirstPrompt();
778
- restoreQueue(error);
779
- });
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
+ }
780
798
 
781
799
  for (const message of rest) {
782
800
  await this.#deliverQueuedMessage(message);