@oh-my-pi/pi-coding-agent 15.12.2 → 15.12.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 (231) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/cli.js +1121 -871
  3. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  4. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  5. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  6. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  7. package/dist/types/cli/args.d.ts +0 -1
  8. package/dist/types/cli/models-cli.d.ts +49 -0
  9. package/dist/types/commands/launch.d.ts +0 -3
  10. package/dist/types/commands/models.d.ts +33 -0
  11. package/dist/types/commands/token.d.ts +25 -0
  12. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  13. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  14. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  15. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  18. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  19. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  20. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  21. package/dist/types/commit/changelog/generate.d.ts +1 -1
  22. package/dist/types/commit/shared-llm.d.ts +1 -1
  23. package/dist/types/config/model-registry.d.ts +7 -0
  24. package/dist/types/config/models-config-schema.d.ts +1 -1
  25. package/dist/types/config/settings-schema.d.ts +21 -1
  26. package/dist/types/edit/hashline/params.d.ts +1 -1
  27. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  28. package/dist/types/edit/modes/patch.d.ts +1 -1
  29. package/dist/types/edit/modes/replace.d.ts +1 -1
  30. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  31. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  33. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  34. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  35. package/dist/types/lsp/types.d.ts +1 -1
  36. package/dist/types/mcp/manager.d.ts +8 -0
  37. package/dist/types/mnemopi/config.d.ts +28 -0
  38. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  39. package/dist/types/modes/components/index.d.ts +1 -0
  40. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  41. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  43. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  44. package/dist/types/modes/interactive-mode.d.ts +0 -3
  45. package/dist/types/modes/types.d.ts +0 -5
  46. package/dist/types/session/agent-session.d.ts +14 -33
  47. package/dist/types/session/agent-storage.d.ts +2 -1
  48. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  49. package/dist/types/session/messages.d.ts +8 -10
  50. package/dist/types/session/session-manager.d.ts +15 -0
  51. package/dist/types/session/session-storage.d.ts +5 -0
  52. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  53. package/dist/types/task/types.d.ts +1 -1
  54. package/dist/types/tools/ask.d.ts +1 -1
  55. package/dist/types/tools/ast-edit.d.ts +1 -1
  56. package/dist/types/tools/ast-grep.d.ts +1 -1
  57. package/dist/types/tools/bash.d.ts +1 -1
  58. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  59. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  60. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  61. package/dist/types/tools/browser/registry.d.ts +16 -3
  62. package/dist/types/tools/browser/render.d.ts +2 -0
  63. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  65. package/dist/types/tools/browser.d.ts +3 -1
  66. package/dist/types/tools/checkpoint.d.ts +1 -1
  67. package/dist/types/tools/debug.d.ts +1 -1
  68. package/dist/types/tools/eval.d.ts +1 -1
  69. package/dist/types/tools/find.d.ts +1 -1
  70. package/dist/types/tools/gh.d.ts +1 -1
  71. package/dist/types/tools/image-gen.d.ts +1 -1
  72. package/dist/types/tools/index.d.ts +3 -1
  73. package/dist/types/tools/inspect-image.d.ts +1 -1
  74. package/dist/types/tools/irc.d.ts +1 -1
  75. package/dist/types/tools/job.d.ts +1 -1
  76. package/dist/types/tools/memory-edit.d.ts +1 -1
  77. package/dist/types/tools/memory-recall.d.ts +1 -1
  78. package/dist/types/tools/memory-reflect.d.ts +1 -1
  79. package/dist/types/tools/memory-retain.d.ts +1 -1
  80. package/dist/types/tools/read.d.ts +1 -1
  81. package/dist/types/tools/render-mermaid.d.ts +1 -1
  82. package/dist/types/tools/resolve.d.ts +1 -1
  83. package/dist/types/tools/review.d.ts +1 -1
  84. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  85. package/dist/types/tools/search.d.ts +1 -1
  86. package/dist/types/tools/ssh.d.ts +1 -1
  87. package/dist/types/tools/todo.d.ts +1 -1
  88. package/dist/types/tools/tts.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +1 -1
  90. package/dist/types/utils/clipboard.d.ts +4 -3
  91. package/dist/types/utils/image-loading.d.ts +18 -1
  92. package/dist/types/utils/thinking-display.d.ts +17 -0
  93. package/dist/types/web/search/index.d.ts +1 -1
  94. package/package.json +14 -14
  95. package/src/autoresearch/storage.ts +2 -1
  96. package/src/autoresearch/tools/init-experiment.ts +1 -1
  97. package/src/autoresearch/tools/log-experiment.ts +1 -1
  98. package/src/autoresearch/tools/run-experiment.ts +1 -1
  99. package/src/autoresearch/tools/update-notes.ts +1 -1
  100. package/src/cli/args.ts +0 -8
  101. package/src/cli/auth-gateway-cli.ts +1 -1
  102. package/src/cli/bench-cli.ts +1 -1
  103. package/src/cli/dry-balance-cli.ts +1 -1
  104. package/src/cli/models-cli.ts +427 -0
  105. package/src/cli-commands.ts +2 -0
  106. package/src/collab/host.ts +9 -12
  107. package/src/commands/launch.ts +0 -3
  108. package/src/commands/models.ts +61 -0
  109. package/src/commands/token.ts +89 -0
  110. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  111. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  112. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  113. package/src/commit/agentic/tools/git-overview.ts +1 -1
  114. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  115. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  116. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  117. package/src/commit/agentic/tools/schemas.ts +1 -1
  118. package/src/commit/agentic/tools/split-commit.ts +1 -1
  119. package/src/commit/analysis/summary.ts +1 -1
  120. package/src/commit/changelog/generate.ts +1 -1
  121. package/src/commit/shared-llm.ts +1 -1
  122. package/src/config/model-registry.ts +15 -12
  123. package/src/config/model-resolver.ts +2 -2
  124. package/src/config/models-config-schema.ts +1 -1
  125. package/src/config/settings-schema.ts +19 -1
  126. package/src/edit/hashline/params.ts +1 -1
  127. package/src/edit/modes/apply-patch.ts +1 -1
  128. package/src/edit/modes/patch.ts +1 -1
  129. package/src/edit/modes/replace.ts +1 -1
  130. package/src/eval/agent-bridge.ts +1 -1
  131. package/src/eval/completion-bridge.ts +1 -1
  132. package/src/export/html/template.js +24 -2
  133. package/src/export/html/tool-views.generated.js +2 -2
  134. package/src/extensibility/custom-commands/loader.ts +1 -1
  135. package/src/extensibility/custom-commands/types.ts +2 -2
  136. package/src/extensibility/custom-tools/loader.ts +1 -1
  137. package/src/extensibility/custom-tools/types.ts +2 -2
  138. package/src/extensibility/extensions/loader.ts +2 -2
  139. package/src/extensibility/extensions/types.ts +2 -2
  140. package/src/extensibility/hooks/loader.ts +1 -1
  141. package/src/extensibility/hooks/types.ts +2 -2
  142. package/src/extensibility/skills.ts +18 -3
  143. package/src/goals/tools/goal-tool.ts +1 -1
  144. package/src/internal-urls/docs-index.generated.ts +6 -3
  145. package/src/lsp/types.ts +1 -1
  146. package/src/main.ts +0 -25
  147. package/src/mcp/config-writer.ts +7 -3
  148. package/src/mcp/manager.ts +11 -0
  149. package/src/memories/index.ts +3 -1
  150. package/src/memories/storage.ts +2 -1
  151. package/src/mnemopi/config.ts +95 -11
  152. package/src/modes/acp/acp-agent.ts +5 -48
  153. package/src/modes/acp/acp-event-mapper.ts +5 -1
  154. package/src/modes/components/agent-hub.ts +2 -1
  155. package/src/modes/components/assistant-message.ts +8 -7
  156. package/src/modes/components/index.ts +1 -0
  157. package/src/modes/components/logout-account-selector.ts +130 -0
  158. package/src/modes/components/mcp-add-wizard.ts +1 -1
  159. package/src/modes/components/model-selector.ts +2 -2
  160. package/src/modes/components/status-line/component.ts +54 -157
  161. package/src/modes/components/status-line/segments.ts +1 -1
  162. package/src/modes/components/status-line/types.ts +2 -1
  163. package/src/modes/controllers/command-controller.ts +0 -12
  164. package/src/modes/controllers/event-controller.ts +23 -62
  165. package/src/modes/controllers/input-controller.ts +60 -31
  166. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  167. package/src/modes/controllers/selector-controller.ts +56 -10
  168. package/src/modes/controllers/streaming-reveal.ts +4 -3
  169. package/src/modes/interactive-mode.ts +2 -8
  170. package/src/modes/theme/theme.ts +1 -1
  171. package/src/modes/types.ts +0 -5
  172. package/src/modes/utils/ui-helpers.ts +2 -1
  173. package/src/prompts/system/empty-stop-retry.md +4 -6
  174. package/src/sdk.ts +15 -19
  175. package/src/session/agent-session.ts +125 -234
  176. package/src/session/agent-storage.ts +18 -9
  177. package/src/session/history-storage.ts +2 -1
  178. package/src/session/indexed-session-storage.ts +7 -0
  179. package/src/session/messages.ts +9 -11
  180. package/src/session/session-dump-format.ts +4 -2
  181. package/src/session/session-manager.ts +116 -0
  182. package/src/session/session-storage.ts +20 -0
  183. package/src/slash-commands/builtin-registry.ts +15 -1
  184. package/src/slash-commands/helpers/logout.ts +88 -0
  185. package/src/task/types.ts +1 -1
  186. package/src/tools/ask.ts +1 -1
  187. package/src/tools/ast-edit.ts +13 -4
  188. package/src/tools/ast-grep.ts +1 -1
  189. package/src/tools/bash.ts +1 -1
  190. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  191. package/src/tools/browser/cmux/rpc.ts +156 -0
  192. package/src/tools/browser/cmux/socket-client.ts +309 -0
  193. package/src/tools/browser/registry.ts +37 -3
  194. package/src/tools/browser/render.ts +6 -1
  195. package/src/tools/browser/tab-protocol.ts +2 -0
  196. package/src/tools/browser/tab-supervisor.ts +189 -18
  197. package/src/tools/browser/tab-worker.ts +1 -1
  198. package/src/tools/browser.ts +16 -1
  199. package/src/tools/checkpoint.ts +1 -1
  200. package/src/tools/debug.ts +1 -1
  201. package/src/tools/eval.ts +11 -6
  202. package/src/tools/fetch.ts +13 -2
  203. package/src/tools/find.ts +1 -1
  204. package/src/tools/gh.ts +1 -1
  205. package/src/tools/github-cache.ts +2 -1
  206. package/src/tools/image-gen.ts +1 -1
  207. package/src/tools/index.ts +3 -1
  208. package/src/tools/inspect-image.ts +3 -1
  209. package/src/tools/irc.ts +1 -1
  210. package/src/tools/job.ts +1 -1
  211. package/src/tools/memory-edit.ts +1 -1
  212. package/src/tools/memory-recall.ts +1 -1
  213. package/src/tools/memory-reflect.ts +1 -1
  214. package/src/tools/memory-retain.ts +1 -1
  215. package/src/tools/read.ts +8 -2
  216. package/src/tools/render-mermaid.ts +1 -1
  217. package/src/tools/report-tool-issue.ts +3 -2
  218. package/src/tools/resolve.ts +1 -1
  219. package/src/tools/review.ts +1 -1
  220. package/src/tools/search-tool-bm25.ts +1 -1
  221. package/src/tools/search.ts +1 -1
  222. package/src/tools/ssh.ts +1 -1
  223. package/src/tools/todo.ts +1 -1
  224. package/src/tools/tts.ts +1 -1
  225. package/src/tools/write.ts +1 -1
  226. package/src/utils/clipboard.ts +35 -18
  227. package/src/utils/image-loading.ts +35 -4
  228. package/src/utils/thinking-display.ts +37 -0
  229. package/src/web/search/index.ts +1 -1
  230. package/dist/types/cli/list-models.d.ts +0 -30
  231. package/src/cli/list-models.ts +0 -194
@@ -120,10 +120,38 @@ export class InputController {
120
120
  });
121
121
  }
122
122
  this.ctx.editor.onEscape = () => {
123
+ // Active context maintenance owns Esc: auto/manual compaction,
124
+ // handoff generation, and auto-retry backoff all advertise
125
+ // "(esc to cancel)". Dispatch on live session state instead of
126
+ // swapping onEscape handlers — interleaved start/end events used
127
+ // to clobber the single saved-handler slot (auto-compaction start
128
+ // → /compact → auto end → manual finally), leaving Esc wired to a
129
+ // stale no-op closure until restart.
130
+ const viewSession = this.ctx.viewSession;
131
+ let aborted = false;
132
+ if (viewSession.isCompacting) {
133
+ try {
134
+ viewSession.abortCompaction();
135
+ } catch {}
136
+ aborted = true;
137
+ }
138
+ if (viewSession.isGeneratingHandoff) {
139
+ try {
140
+ viewSession.abortHandoff();
141
+ } catch {}
142
+ aborted = true;
143
+ }
144
+ if (viewSession.isRetrying) {
145
+ try {
146
+ viewSession.abortRetry();
147
+ } catch {}
148
+ aborted = true;
149
+ }
150
+ if (aborted) return;
151
+
123
152
  if (this.ctx.loopModeEnabled) {
124
153
  this.ctx.pauseLoop();
125
154
  if (this.ctx.session.isStreaming) {
126
- this.ctx.notifyInterrupting();
127
155
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
128
156
  } else {
129
157
  this.ctx.cancelPendingSubmission();
@@ -153,7 +181,6 @@ export class InputController {
153
181
  // session is never streaming, so the native abort path below would
154
182
  // no-op.
155
183
  if (this.ctx.collabGuest.state?.isStreaming || this.ctx.loadingAnimation) {
156
- if (!this.ctx.collabGuest.readOnly) this.ctx.notifyInterrupting();
157
184
  this.ctx.collabGuest.sendAbort();
158
185
  }
159
186
  return;
@@ -176,9 +203,13 @@ export class InputController {
176
203
  this.ctx.isPythonMode = false;
177
204
  this.ctx.updateEditorBorderColor();
178
205
  } else if (this.ctx.session.isStreaming) {
179
- this.ctx.notifyInterrupting();
180
206
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
181
- } else if (!this.ctx.editor.getText().trim()) {
207
+ } else if (this.ctx.editor.getText().trim()) {
208
+ // Esc with typed text clears the draft instead of (or before) any double-Esc action
209
+ this.ctx.editor.setText("");
210
+ this.ctx.ui.requestRender();
211
+ this.ctx.lastEscapeTime = 0;
212
+ } else {
182
213
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
183
214
  const action = settings.get("doubleEscapeAction");
184
215
  if (action !== "none") {
@@ -189,6 +220,7 @@ export class InputController {
189
220
  } else {
190
221
  this.ctx.showUserMessageSelector();
191
222
  }
223
+ this.ctx.ui.resetDisplay();
192
224
  this.ctx.lastEscapeTime = 0;
193
225
  } else {
194
226
  this.ctx.lastEscapeTime = now;
@@ -370,22 +402,16 @@ export class InputController {
370
402
  return;
371
403
  }
372
404
 
373
- // Empty submit while streaming with queued steering: interrupt now and
374
- // immediately resume so the visible `Steer:` entry is sent without
375
- // waiting for the current tool/model boundary.
405
+ // Empty submit while streaming with queued messages: abort the active
406
+ // turn and let the post-unwind drain deliver the agent-core queue.
376
407
  if (!text && this.ctx.session.isStreaming) {
377
- const queuedMessages = this.ctx.session.getQueuedMessages();
378
- if (queuedMessages.steering.length > 0) {
379
- await this.ctx.session.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
408
+ if (this.ctx.session.queuedMessageCount > 0) {
409
+ const aborting = this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
410
+ await aborting;
380
411
  this.ctx.updatePendingMessagesDisplay();
381
412
  this.ctx.ui.requestRender();
382
- return;
383
- }
384
- if (this.ctx.session.queuedMessageCount > 0) {
385
- // Preserve the existing empty-submit flush for non-steer queues.
386
- await this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
387
- return;
388
413
  }
414
+ return;
389
415
  }
390
416
 
391
417
  if (!text) return;
@@ -657,9 +683,9 @@ export class InputController {
657
683
  async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
658
684
  const target = this.ctx.viewSession;
659
685
  if (!text) {
660
- // Mirror the empty-submit steer flush against the focused session.
661
- if (target.isStreaming && target.getQueuedMessages().steering.length > 0) {
662
- await target.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
686
+ if (target.isStreaming && target.queuedMessageCount > 0) {
687
+ const aborting = target.abort({ reason: USER_INTERRUPT_LABEL });
688
+ await aborting;
663
689
  this.ctx.updatePendingMessagesDisplay();
664
690
  this.ctx.ui.requestRender();
665
691
  }
@@ -696,6 +722,18 @@ export class InputController {
696
722
  this.ctx.clearEditor();
697
723
  this.ctx.lastSigintTime = now;
698
724
  }
725
+ // Sync-flush the session JSONL so in-flight writes survive a hard exit.
726
+ // The TUI consumes Ctrl+C as a key event in raw mode, so postmortem's
727
+ // process-level SIGINT handler never fires. The second press still
728
+ // funnels through shutdown() which awaits its own async flush — the
729
+ // sync flush here is a superset that also covers the first-press case.
730
+ try {
731
+ this.ctx.sessionManager.flushSync();
732
+ } catch (err) {
733
+ logger.warn("session-manager sync flush on Ctrl+C failed", {
734
+ error: err instanceof Error ? err.message : String(err),
735
+ });
736
+ }
699
737
  }
700
738
 
701
739
  handleCtrlD(): void {
@@ -792,15 +830,6 @@ export class InputController {
792
830
  args: args || undefined,
793
831
  lineCount: body ? body.split("\n").length : 0,
794
832
  };
795
- // When the agent is streaming, register the compact slash-form text as
796
- // the pending-display twin BEFORE dispatching the CustomMessage. The
797
- // returned tag is embedded in details so AgentSession.#handleAgentEvent
798
- // can remove the matching display entry when the agent consumes this
799
- // message (mirrors the user-message dequeue path).
800
- if (this.ctx.session.isStreaming) {
801
- const tag = this.ctx.session.enqueueCustomMessageDisplay(text, streamingBehavior);
802
- details.__pendingDisplayTag = tag;
803
- }
804
833
  await this.ctx.session.promptCustomMessage(
805
834
  {
806
835
  customType: SKILL_PROMPT_MESSAGE_TYPE,
@@ -809,7 +838,7 @@ export class InputController {
809
838
  details,
810
839
  attribution: "user",
811
840
  },
812
- { streamingBehavior },
841
+ { streamingBehavior, queueChipText: text },
813
842
  );
814
843
  if (this.ctx.session.isStreaming) {
815
844
  this.ctx.updatePendingMessagesDisplay();
@@ -899,7 +928,7 @@ export class InputController {
899
928
  if (allQueued.length === 0) {
900
929
  this.ctx.updatePendingMessagesDisplay();
901
930
  if (options?.abort) {
902
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
931
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
903
932
  }
904
933
  return 0;
905
934
  }
@@ -918,7 +947,7 @@ export class InputController {
918
947
  }
919
948
  this.ctx.updatePendingMessagesDisplay();
920
949
  if (options?.abort) {
921
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
950
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
922
951
  }
923
952
  return allQueued.length;
924
953
  }
@@ -813,6 +813,43 @@ export class MCPCommandController {
813
813
  return null;
814
814
  }
815
815
 
816
+ /**
817
+ * Resolve a server for an auth/test operation.
818
+ *
819
+ * Unlike {@link #findConfiguredServer} (which only reads writable OMP config
820
+ * files), this also recognizes runtime-discovered servers that `/mcp list`
821
+ * surfaces but that live in no writable config — e.g. servers from a Claude
822
+ * Code marketplace plugin (`cloudflare:cloudflare-api`), `.cursor/mcp.json`,
823
+ * etc. Without this, `/mcp reauth|test|unauth` reports "not found" for a
824
+ * server the list just showed.
825
+ *
826
+ * For a discovered server, any persisted change is written into the *user*
827
+ * config under the same (namespaced) name; the native provider (priority 100)
828
+ * shadows the discovered entry on the next reload, so an OAuth `auth` block
829
+ * persisted by `/mcp reauth` takes effect. `discovered` lets callers tailor
830
+ * messaging and skip pointless writes when there is nothing to persist.
831
+ */
832
+ async #resolveServerForAuth(name: string): Promise<{
833
+ filePath: string;
834
+ scope: "user" | "project";
835
+ config: MCPServerConfig;
836
+ discovered: boolean;
837
+ } | null> {
838
+ const found = await this.#findConfiguredServer(name);
839
+ if (found) return { ...found, discovered: false };
840
+
841
+ const config = this.ctx.mcpManager?.getServerConfig(name);
842
+ const source = this.ctx.mcpManager?.getSource(name);
843
+ if (!config || !source) return null;
844
+
845
+ return {
846
+ filePath: getMCPConfigPath("user", getProjectDir()),
847
+ scope: "user",
848
+ config,
849
+ discovered: true,
850
+ };
851
+ }
852
+
816
853
  async #removeManagedOAuthCredential(credentialId: string | undefined): Promise<void> {
817
854
  if (!credentialId?.startsWith("mcp_oauth_")) return;
818
855
  await this.ctx.session.modelRegistry.authStorage.remove(credentialId);
@@ -1199,7 +1236,7 @@ export class MCPCommandController {
1199
1236
 
1200
1237
  let connection: MCPServerConnection | undefined;
1201
1238
  try {
1202
- const found = await this.#findConfiguredServer(name);
1239
+ const found = await this.#resolveServerForAuth(name);
1203
1240
 
1204
1241
  if (!found) {
1205
1242
  this.ctx.showError(
@@ -1389,13 +1426,17 @@ export class MCPCommandController {
1389
1426
  }
1390
1427
 
1391
1428
  try {
1392
- const found = await this.#findConfiguredServer(name);
1429
+ const found = await this.#resolveServerForAuth(name);
1393
1430
  if (!found) {
1394
1431
  this.ctx.showError(`Server "${name}" not found.`);
1395
1432
  return;
1396
1433
  }
1397
1434
 
1398
1435
  const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
1436
+ if (found.discovered && currentAuth?.type !== "oauth") {
1437
+ this.#showMessage(["", theme.fg("muted", `No stored OAuth auth to remove for "${name}".`), ""].join("\n"));
1438
+ return;
1439
+ }
1399
1440
  if (currentAuth?.type === "oauth") {
1400
1441
  await this.#removeManagedOAuthCredential(currentAuth.credentialId);
1401
1442
  }
@@ -1419,7 +1460,7 @@ export class MCPCommandController {
1419
1460
  }
1420
1461
 
1421
1462
  try {
1422
- const found = await this.#findConfiguredServer(name);
1463
+ const found = await this.#resolveServerForAuth(name);
1423
1464
  if (!found) {
1424
1465
  this.ctx.showError(`Server "${name}" not found.`);
1425
1466
  return;
@@ -30,6 +30,7 @@ import type { InteractiveModeContext } from "../../modes/types";
30
30
  import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
31
31
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
32
32
  import { FileSessionStorage } from "../../session/session-storage";
33
+ import { type LogoutAccount, toLogoutAccounts } from "../../slash-commands/helpers/logout";
33
34
  import {
34
35
  describeRedeemOutcome,
35
36
  type ResetUsageAccount,
@@ -51,6 +52,7 @@ import { AssistantMessageComponent } from "../components/assistant-message";
51
52
  import { CopySelectorComponent } from "../components/copy-selector";
52
53
  import { ExtensionDashboard } from "../components/extensions";
53
54
  import { HistorySearchComponent } from "../components/history-search";
55
+ import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
54
56
  import { ModelSelectorComponent } from "../components/model-selector";
55
57
  import { OAuthSelectorComponent } from "../components/oauth-selector";
56
58
  import { PluginSelectorComponent } from "../components/plugin-selector";
@@ -1000,23 +1002,28 @@ export class SelectorController {
1000
1002
  }
1001
1003
  }
1002
1004
 
1003
- async #handleOAuthLogout(providerId: string): Promise<void> {
1005
+ async #handleCredentialLogout(providerId: string, account: LogoutAccount): Promise<void> {
1004
1006
  try {
1005
1007
  const authStorage = this.ctx.session.modelRegistry.authStorage;
1006
- if (!authStorage.has(providerId)) {
1007
- const source = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
1008
- const suffix = source ? ` Current auth comes from ${source}; remove that source to log out.` : "";
1009
- this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
1008
+ const removed = await authStorage.removeCredential(providerId, account.credentialId);
1009
+ if (!removed) {
1010
+ this.ctx.showError(`Logout skipped: ${account.label} is no longer stored for ${providerId}.`);
1010
1011
  return;
1011
1012
  }
1012
1013
 
1013
- await authStorage.logout(providerId);
1014
1014
  await this.ctx.session.modelRegistry.refresh();
1015
1015
  const block = new TranscriptBlock();
1016
1016
  block.addChild(
1017
- new Text(theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`), 1, 0),
1017
+ new Text(
1018
+ theme.fg(
1019
+ "success",
1020
+ `${theme.status.success} Successfully logged out ${account.label} from ${providerId}`,
1021
+ ),
1022
+ 1,
1023
+ 0,
1024
+ ),
1018
1025
  );
1019
- block.addChild(new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0));
1026
+ block.addChild(new Text(theme.fg("dim", `Credential removed from ${getAgentDbPath()}`), 1, 0));
1020
1027
  const remainingSource = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
1021
1028
  if (remainingSource) {
1022
1029
  block.addChild(
@@ -1029,12 +1036,51 @@ export class SelectorController {
1029
1036
  }
1030
1037
  }
1031
1038
 
1039
+ async #showOAuthLogoutAccountSelector(providerId: string): Promise<void> {
1040
+ const authStorage = this.ctx.session.modelRegistry.authStorage;
1041
+ try {
1042
+ await authStorage.reload();
1043
+ } catch (error: unknown) {
1044
+ this.ctx.showError(
1045
+ `Could not load stored credentials: ${error instanceof Error ? error.message : String(error)}`,
1046
+ );
1047
+ return;
1048
+ }
1049
+ const provider = getOAuthProviders().find(candidate => candidate.id === providerId);
1050
+ const accounts = toLogoutAccounts(providerId, authStorage.listStoredCredentials(providerId), {
1051
+ activeIdentity: authStorage.getOAuthAccountIdentity(providerId, this.ctx.session.sessionId),
1052
+ activeApiKey: authStorage.getCredentialOrigin(providerId)?.kind === "api_key",
1053
+ });
1054
+ if (accounts.length === 0) {
1055
+ const source = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
1056
+ const suffix = source ? ` Current auth comes from ${source}; remove that source to log out.` : "";
1057
+ this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
1058
+ return;
1059
+ }
1060
+
1061
+ this.showSelector(done => {
1062
+ const selector = new LogoutAccountSelectorComponent(
1063
+ provider?.name ?? providerId,
1064
+ accounts,
1065
+ account => {
1066
+ done();
1067
+ void this.#handleCredentialLogout(providerId, account);
1068
+ },
1069
+ () => {
1070
+ done();
1071
+ this.ctx.ui.requestRender();
1072
+ },
1073
+ );
1074
+ return { component: selector, focus: selector };
1075
+ });
1076
+ }
1077
+
1032
1078
  async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
1033
1079
  if (providerId) {
1034
1080
  if (mode === "login") {
1035
1081
  await this.#handleOAuthLogin(providerId);
1036
1082
  } else {
1037
- await this.#handleOAuthLogout(providerId);
1083
+ await this.#showOAuthLogoutAccountSelector(providerId);
1038
1084
  }
1039
1085
  return;
1040
1086
  }
@@ -1062,7 +1108,7 @@ export class SelectorController {
1062
1108
  if (mode === "login") {
1063
1109
  await this.#handleOAuthLogin(selectedProviderId);
1064
1110
  } else {
1065
- await this.#handleOAuthLogout(selectedProviderId);
1111
+ await this.#showOAuthLogoutAccountSelector(selectedProviderId);
1066
1112
  }
1067
1113
  },
1068
1114
  () => {
@@ -1,6 +1,7 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { getSegmenter } from "@oh-my-pi/pi-tui";
3
3
  import { LRUCache } from "lru-cache/raw";
4
+ import { hasVisibleThinking } from "../../utils/thinking-display";
4
5
  import type { AssistantMessageComponent } from "../components/assistant-message";
5
6
 
6
7
  export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
@@ -87,7 +88,7 @@ export function visibleUnits(message: AssistantMessage, hideThinking: boolean):
87
88
  for (const block of message.content) {
88
89
  if (block.type === "text") {
89
90
  total += countGraphemes(block.text);
90
- } else if (block.type === "thinking" && !hideThinking) {
91
+ } else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
91
92
  total += countGraphemes(block.thinking);
92
93
  }
93
94
  }
@@ -128,7 +129,7 @@ export function buildDisplayMessage(
128
129
  const units = countOf(i, block.text);
129
130
  content.push(revealTextBlock(block, remaining, units));
130
131
  remaining = Math.max(0, remaining - units);
131
- } else if (block.type === "thinking" && !hideThinking) {
132
+ } else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
132
133
  const units = countOf(i, block.thinking);
133
134
  content.push(revealThinkingBlock(block, remaining, units));
134
135
  remaining = Math.max(0, remaining - units);
@@ -230,7 +231,7 @@ export class StreamingRevealController {
230
231
  const block = message.content[i]!;
231
232
  if (block.type === "text") {
232
233
  total += this.#unitCounter.count(i, block.text);
233
- } else if (block.type === "thinking" && !this.#hideThinkingBlock) {
234
+ } else if (block.type === "thinking" && !this.#hideThinkingBlock && hasVisibleThinking(block)) {
234
235
  total += this.#unitCounter.count(i, block.thinking);
235
236
  }
236
237
  }
@@ -383,8 +383,6 @@ export class InteractiveMode implements InteractiveModeContext {
383
383
  get #defaultWorkingMessage(): string {
384
384
  return `Working…${interruptHint()}`;
385
385
  }
386
- autoCompactionEscapeHandler?: () => void;
387
- retryEscapeHandler?: () => void;
388
386
  unsubscribe?: () => void;
389
387
  onInputCallback?: (input: SubmittedUserInput) => void;
390
388
  optimisticUserMessageSignature: string | undefined = undefined;
@@ -2437,7 +2435,7 @@ export class InteractiveMode implements InteractiveModeContext {
2437
2435
  async #startGoalFromObjective(objective: string): Promise<void> {
2438
2436
  await this.#enterGoalMode({ objective, silent: true });
2439
2437
  this.#resetGoalContinuationSuppression();
2440
- if (this.onInputCallback) {
2438
+ if (!this.session.isStreaming && this.onInputCallback) {
2441
2439
  this.onInputCallback(this.startPendingSubmission({ text: objective }));
2442
2440
  }
2443
2441
  }
@@ -2452,7 +2450,7 @@ export class InteractiveMode implements InteractiveModeContext {
2452
2450
  if (this.session.isStreaming) {
2453
2451
  await this.session.sendGoalModeContext({ deliverAs: "steer" });
2454
2452
  }
2455
- if (this.onInputCallback) {
2453
+ if (!this.session.isStreaming && this.onInputCallback) {
2456
2454
  this.onInputCallback(this.startPendingSubmission({ text: objective }));
2457
2455
  }
2458
2456
  }
@@ -3025,10 +3023,6 @@ export class InteractiveMode implements InteractiveModeContext {
3025
3023
  this.setWorkingMessage(message);
3026
3024
  }
3027
3025
 
3028
- notifyInterrupting(): void {
3029
- this.#eventController.notifyInterrupting();
3030
- }
3031
-
3032
3026
  showNewVersionNotification(newVersion: string): void {
3033
3027
  this.#uiHelpers.showNewVersionNotification(newVersion);
3034
3028
  }
@@ -13,7 +13,7 @@ import type { EditorTheme, MarkdownTheme, SelectListTheme, SettingsListTheme, Sy
13
13
  import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
14
14
  import chalk from "chalk";
15
15
  import { LRUCache } from "lru-cache/raw";
16
- import * as z from "zod/v4";
16
+ import { z } from "zod/v4";
17
17
  // Embed theme JSON files at build time
18
18
  import darkThemeJson from "./dark.json" with { type: "json" };
19
19
  import { defaultThemes } from "./defaults";
@@ -149,8 +149,6 @@ export interface InteractiveModeContext {
149
149
  loadingAnimation: Loader | undefined;
150
150
  autoCompactionLoader: Loader | undefined;
151
151
  retryLoader: Loader | undefined;
152
- autoCompactionEscapeHandler?: () => void;
153
- retryEscapeHandler?: () => void;
154
152
  unsubscribe?: () => void;
155
153
  onInputCallback?: (input: SubmittedUserInput) => void;
156
154
  optimisticUserMessageSignature: string | undefined;
@@ -211,9 +209,6 @@ export interface InteractiveModeContext {
211
209
  flushPendingModelSwitch(): Promise<void>;
212
210
  setWorkingMessage(message?: string): void;
213
211
  applyPendingWorkingMessage(): void;
214
- /** Acknowledge a user interrupt (Esc) by switching the loader to an
215
- * "Interrupting…" label until the agent turn unwinds. */
216
- notifyInterrupting(): void;
217
212
  ensureLoadingAnimation(): void;
218
213
  startPendingSubmission(input: {
219
214
  text: string;
@@ -39,6 +39,7 @@ import {
39
39
  import type { SessionContext } from "../../session/session-manager";
40
40
  import { createIrcMessageCard } from "../../tools/irc";
41
41
  import { formatBytes, formatDuration } from "../../tools/render-utils";
42
+ import { hasVisibleThinking } from "../../utils/thinking-display";
42
43
 
43
44
  type TextBlock = { type: "text"; text: string };
44
45
  interface RenderInitialMessagesOptions {
@@ -367,7 +368,7 @@ export class UiHelpers {
367
368
  const hasVisibleAssistantContent = message.content.some(
368
369
  content =>
369
370
  (content.type === "text" && content.text.trim().length > 0) ||
370
- (content.type === "thinking" && content.thinking.trim().length > 0),
371
+ (content.type === "thinking" && hasVisibleThinking(content)),
371
372
  );
372
373
  if (hasVisibleAssistantContent) {
373
374
  // Rebuild reconstructs immutable history; seal (not finalize) so the
@@ -1,6 +1,4 @@
1
- <system-reminder>
2
- The previous assistant turn ended with no text, reasoning, or tool call.
3
- Continue the active task from the current context. If the work is complete, reply with a concise final summary instead of an empty response.
4
-
5
- (Empty response retry {{retryCount}}/{{maxRetries}})
6
- </system-reminder>
1
+ <system-injection>
2
+ You stopped without completing the task. Continue.
3
+ Attempt #{{retryCount}}/{{maxRetries}}
4
+ </system-injection>
package/src/sdk.ts CHANGED
@@ -1166,20 +1166,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1166
1166
  SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
1167
1167
  );
1168
1168
  const providerSessionId = options.providerSessionId ?? sessionManager.getSessionId();
1169
- const modelApiKeyAvailability = new Map<string, boolean>();
1170
- const getModelAvailabilityKey = (candidate: Model): string =>
1171
- `${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
1172
- const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
1173
- const availabilityKey = getModelAvailabilityKey(candidate);
1174
- const cached = modelApiKeyAvailability.get(availabilityKey);
1175
- if (cached !== undefined) {
1176
- return cached;
1177
- }
1178
-
1179
- const hasKey = !!(await modelRegistry.getApiKey(candidate, providerSessionId));
1180
- modelApiKeyAvailability.set(availabilityKey, hasKey);
1181
- return hasKey;
1182
- };
1169
+ // Startup model *selection* only needs to know whether auth is configured for
1170
+ // a candidate's provider never the resolved key bytes. Use the synchronous,
1171
+ // side-effect-free probe (`hasConfiguredAuth`): it refreshes no OAuth tokens,
1172
+ // executes no `!command` keys, and issues no auth-broker requests. Resolving the
1173
+ // real key here (`getApiKey`) blocks resume on those network paths — a slow or
1174
+ // unreachable OAuth/broker endpoint stalls startup for the full ~10s refresh
1175
+ // timeout per candidate (observed as a hang in `restoreSessionModel`). The real
1176
+ // key is resolved lazily per request via ModelRegistry.resolver.
1177
+ const hasModelAuth = (candidate: Model): boolean => modelRegistry.hasConfiguredAuth(candidate);
1183
1178
 
1184
1179
  // Load and create secret obfuscator early so resumed session state and prompt warnings
1185
1180
  // reflect actual loaded secrets, not just the setting toggle.
@@ -1228,7 +1223,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1228
1223
  : [];
1229
1224
  let restoredSessionModelIndex = -1;
1230
1225
  if (!hasExplicitModel && !model && sessionModelStrings.length > 0) {
1231
- await logger.time("restoreSessionModel", async () => {
1226
+ logger.time("restoreSessionModel", () => {
1232
1227
  let failedSessionModel: string | undefined;
1233
1228
  for (let i = 0; i < sessionModelStrings.length; i++) {
1234
1229
  const sessionModelStr = sessionModelStrings[i];
@@ -1239,7 +1234,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1239
1234
  }
1240
1235
 
1241
1236
  const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
1242
- if (restoredModel && (await hasModelApiKey(restoredModel))) {
1237
+ if (restoredModel && hasModelAuth(restoredModel)) {
1243
1238
  model = restoredModel;
1244
1239
  restoredSessionModelIndex = i;
1245
1240
  break;
@@ -1492,6 +1487,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1492
1487
  getSessionSpawns: () => options.spawns ?? "*",
1493
1488
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
1494
1489
  getActiveModelString,
1490
+ getActiveModel: () => agent?.state.model ?? model,
1495
1491
  getPlanModeState: () => session?.getPlanModeState(),
1496
1492
  getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
1497
1493
  getGoalModeState: () => session?.getGoalModeState(),
@@ -1866,7 +1862,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1866
1862
  const parsedModel = parseModelString(sessionModelStr);
1867
1863
  if (!parsedModel) continue;
1868
1864
  const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
1869
- if (restoredModel && (await hasModelApiKey(restoredModel))) {
1865
+ if (restoredModel && hasModelAuth(restoredModel)) {
1870
1866
  model = restoredModel;
1871
1867
  modelFallbackMessage = undefined;
1872
1868
  restoredSessionModelIndex = i;
@@ -1918,7 +1914,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1918
1914
  const preferred = fallbackCandidates.find(
1919
1915
  candidate => candidate.provider === provider && candidate.id === defaultId,
1920
1916
  );
1921
- if (preferred && (await hasModelApiKey(preferred))) {
1917
+ if (preferred && hasModelAuth(preferred)) {
1922
1918
  model = preferred;
1923
1919
  break;
1924
1920
  }
@@ -1926,7 +1922,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1926
1922
  // Otherwise, first available model with a valid API key.
1927
1923
  if (!model) {
1928
1924
  for (const candidate of fallbackCandidates) {
1929
- if (await hasModelApiKey(candidate)) {
1925
+ if (hasModelAuth(candidate)) {
1930
1926
  model = candidate;
1931
1927
  break;
1932
1928
  }