@oh-my-pi/pi-coding-agent 16.1.7 → 16.1.9

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 (120) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli.js +4266 -3446
  3. package/dist/types/cli/flag-tables.d.ts +17 -0
  4. package/dist/types/cli/session-picker.d.ts +0 -1
  5. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  6. package/dist/types/cli-commands.d.ts +4 -2
  7. package/dist/types/collab/protocol.d.ts +20 -1
  8. package/dist/types/config/settings-schema.d.ts +57 -6
  9. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
  10. package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
  11. package/dist/types/mcp/loader.d.ts +3 -2
  12. package/dist/types/mcp/manager.d.ts +4 -3
  13. package/dist/types/mcp/startup-events.d.ts +21 -4
  14. package/dist/types/mnemopi/config.d.ts +1 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +1 -0
  16. package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  18. package/dist/types/modes/components/btw-panel.d.ts +2 -0
  19. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
  20. package/dist/types/modes/components/session-selector.d.ts +0 -2
  21. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  24. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  25. package/dist/types/modes/interactive-mode.d.ts +4 -1
  26. package/dist/types/modes/types.d.ts +4 -1
  27. package/dist/types/session/agent-session.d.ts +18 -3
  28. package/dist/types/session/session-history-format.d.ts +25 -0
  29. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  30. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  31. package/dist/types/slash-commands/types.d.ts +2 -0
  32. package/dist/types/system-prompt.d.ts +1 -0
  33. package/dist/types/tiny/models.d.ts +11 -7
  34. package/dist/types/tools/browser/launch.d.ts +5 -0
  35. package/dist/types/tools/todo.d.ts +1 -0
  36. package/dist/types/utils/thinking-display.d.ts +2 -0
  37. package/package.json +12 -12
  38. package/src/advisor/__tests__/advisor.test.ts +104 -0
  39. package/src/advisor/runtime.ts +38 -2
  40. package/src/cli/args.ts +0 -1
  41. package/src/cli/flag-tables.ts +42 -0
  42. package/src/cli/profile-bootstrap.ts +1 -11
  43. package/src/cli/session-picker.ts +1 -2
  44. package/src/cli/tiny-models-cli.ts +7 -2
  45. package/src/cli-commands.ts +48 -3
  46. package/src/collab/guest.ts +172 -20
  47. package/src/collab/host.ts +47 -5
  48. package/src/collab/protocol.ts +16 -1
  49. package/src/config/model-registry.ts +6 -7
  50. package/src/config/settings-schema.ts +59 -5
  51. package/src/edit/renderer.ts +8 -12
  52. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/filesystem-resource.ts +34 -0
  55. package/src/internal-urls/local-protocol.ts +7 -1
  56. package/src/internal-urls/memory-protocol.ts +5 -1
  57. package/src/internal-urls/skill-protocol.ts +20 -4
  58. package/src/internal-urls/vault-protocol.ts +5 -2
  59. package/src/lsp/client.ts +39 -25
  60. package/src/main.ts +8 -8
  61. package/src/mcp/loader.ts +4 -3
  62. package/src/mcp/manager.ts +35 -15
  63. package/src/mcp/startup-events.ts +106 -11
  64. package/src/mnemopi/config.ts +2 -0
  65. package/src/mnemopi/state.ts +3 -1
  66. package/src/modes/components/agent-hub.ts +4 -0
  67. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  68. package/src/modes/components/assistant-message.ts +217 -18
  69. package/src/modes/components/btw-panel.ts +15 -3
  70. package/src/modes/components/chat-transcript-builder.ts +8 -2
  71. package/src/modes/components/model-selector.ts +72 -9
  72. package/src/modes/components/omfg-panel.ts +1 -1
  73. package/src/modes/components/session-selector.ts +4 -9
  74. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  75. package/src/modes/components/tool-execution.ts +10 -2
  76. package/src/modes/controllers/btw-controller.ts +32 -7
  77. package/src/modes/controllers/command-controller.ts +18 -3
  78. package/src/modes/controllers/event-controller.ts +24 -4
  79. package/src/modes/controllers/input-controller.ts +43 -21
  80. package/src/modes/controllers/mcp-command-controller.ts +40 -12
  81. package/src/modes/controllers/selector-controller.ts +23 -13
  82. package/src/modes/controllers/streaming-reveal.ts +78 -20
  83. package/src/modes/controllers/todo-command-controller.ts +9 -10
  84. package/src/modes/interactive-mode.ts +84 -9
  85. package/src/modes/types.ts +4 -1
  86. package/src/modes/utils/ui-helpers.ts +1 -0
  87. package/src/prompts/advisor/system.md +1 -1
  88. package/src/prompts/system/side-channel-no-tools.md +3 -0
  89. package/src/sdk.ts +12 -8
  90. package/src/session/agent-session.ts +385 -85
  91. package/src/session/history-storage.ts +15 -16
  92. package/src/session/session-history-format.ts +41 -1
  93. package/src/session/snapcompact-inline.ts +9 -6
  94. package/src/slash-commands/builtin-registry.ts +171 -87
  95. package/src/slash-commands/helpers/todo.ts +12 -6
  96. package/src/slash-commands/types.ts +2 -0
  97. package/src/system-prompt.ts +6 -11
  98. package/src/tiny/models.ts +7 -3
  99. package/src/tiny/title-client.ts +8 -2
  100. package/src/tiny/worker.ts +1 -0
  101. package/src/tools/browser/launch.ts +107 -31
  102. package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
  103. package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
  104. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
  105. package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
  106. package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
  107. package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
  108. package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
  109. package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
  110. package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
  111. package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
  112. package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
  113. package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
  114. package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
  115. package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
  116. package/src/tools/search.ts +10 -1
  117. package/src/tools/sqlite-reader.ts +59 -3
  118. package/src/tools/todo.ts +6 -0
  119. package/src/tools/write.ts +4 -6
  120. package/src/utils/thinking-display.ts +78 -0
@@ -31,7 +31,13 @@ import {
31
31
  JSON_TREE_SCALAR_LEN_EXPANDED,
32
32
  renderJsonTreeLines,
33
33
  } from "../../tools/json-tree";
34
- import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
34
+ import {
35
+ formatExpandHint,
36
+ formatStatusIcon,
37
+ replaceTabs,
38
+ resolveImageOptions,
39
+ truncateToWidth,
40
+ } from "../../tools/render-utils";
35
41
  import { toolRenderers } from "../../tools/renderers";
36
42
  import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
37
43
  import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
@@ -865,9 +871,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
865
871
  this.#multiFileBoxes.push(pendingSpacer);
866
872
  this.addChild(pendingSpacer);
867
873
  const pendingBox = new Box(0, 0);
874
+ const spinner =
875
+ this.#spinnerFrame !== undefined ? formatStatusIcon("running", theme, this.#spinnerFrame) : "";
868
876
  const pendingText = renderStatusLine(
869
877
  {
870
- icon: "pending",
878
+ iconOverride: spinner,
871
879
  title: "Edit",
872
880
  description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
873
881
  },
@@ -1,6 +1,7 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { prompt } from "@oh-my-pi/pi-utils";
3
3
  import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
4
+ import { copyToClipboard } from "../../utils/clipboard";
4
5
  import { BtwPanelComponent } from "../components/btw-panel";
5
6
  import type { InteractiveModeContext } from "../types";
6
7
 
@@ -39,6 +40,8 @@ export class BtwController {
39
40
  #lastAssistantMessage: AssistantMessage | undefined;
40
41
  #lastLeafId: string | null | undefined;
41
42
  #branchInFlight = false;
43
+ #lastCopyText: string | undefined;
44
+ #copyInFlight = false;
42
45
 
43
46
  constructor(private readonly ctx: InteractiveModeContext) {}
44
47
 
@@ -58,6 +61,27 @@ export class BtwController {
58
61
  );
59
62
  }
60
63
 
64
+ canCopy(): boolean {
65
+ return (
66
+ !this.#copyInFlight && this.#activeRequest?.component.isCopyable() === true && this.#lastCopyText !== undefined
67
+ );
68
+ }
69
+
70
+ async handleCopy(): Promise<boolean> {
71
+ if (!this.canCopy() || this.#lastCopyText === undefined) return false;
72
+ this.#copyInFlight = true;
73
+ try {
74
+ await copyToClipboard(this.#lastCopyText);
75
+ this.ctx.showStatus("Copied /btw answer to clipboard");
76
+ return true;
77
+ } catch (error) {
78
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
79
+ return true;
80
+ } finally {
81
+ this.#copyInFlight = false;
82
+ }
83
+ }
84
+
61
85
  async handleBranch(): Promise<boolean> {
62
86
  if (!this.canBranch() || !this.#lastQuestion || !this.#lastAssistantMessage) return false;
63
87
  this.#branchInFlight = true;
@@ -123,17 +147,17 @@ export class BtwController {
123
147
  if (!this.#isActiveRequest(request)) {
124
148
  return;
125
149
  }
126
- if (replyText) {
127
- request.component.setAnswer(replyText);
128
- }
150
+ request.component.setAnswer(replyText);
129
151
  request.component.markComplete();
130
- if (request.component.isBranchable()) {
152
+ const copyText = request.component.getCopyText();
153
+ if (copyText !== undefined) {
131
154
  this.#lastQuestion = request.question;
132
155
  this.#lastReplyText = replyText;
156
+ this.#lastCopyText = copyText;
133
157
  this.#lastAssistantMessage = assistantMessageWithReplyText(assistantMessage, replyText);
134
158
  this.#lastLeafId = request.leafId;
135
159
  } else {
136
- this.#clearBranchState();
160
+ this.#clearCompletedState();
137
161
  }
138
162
  } catch (error) {
139
163
  if (!this.#isActiveRequest(request)) {
@@ -151,7 +175,7 @@ export class BtwController {
151
175
  const request = this.#activeRequest;
152
176
  if (!request) return;
153
177
  this.#activeRequest = undefined;
154
- this.#clearBranchState();
178
+ this.#clearCompletedState();
155
179
  if (options.abort) {
156
180
  request.abortController.abort();
157
181
  }
@@ -160,10 +184,11 @@ export class BtwController {
160
184
  this.ctx.ui.requestRender();
161
185
  }
162
186
 
163
- #clearBranchState(): void {
187
+ #clearCompletedState(): void {
164
188
  this.#lastQuestion = undefined;
165
189
  this.#lastReplyText = undefined;
166
190
  this.#lastAssistantMessage = undefined;
191
+ this.#lastCopyText = undefined;
167
192
  this.#lastLeafId = undefined;
168
193
  }
169
194
 
@@ -85,15 +85,30 @@ export class CommandController {
85
85
  }
86
86
  }
87
87
 
88
- handleDumpCommand() {
88
+ async handleDumpCommand(): Promise<void> {
89
89
  try {
90
90
  const formatted = this.ctx.session.formatSessionAsText();
91
91
  if (!formatted) {
92
92
  this.ctx.showError("No messages to dump yet.");
93
93
  return;
94
94
  }
95
- copyToClipboard(formatted);
96
- this.ctx.showStatus("Session copied to clipboard");
95
+ // Build the LLM request JSON sidecar first so its path (and a
96
+ // raw-context warning) can be appended to the copied transcript.
97
+ let sidecarPath: string | undefined;
98
+ let sidecarError: string | undefined;
99
+ try {
100
+ sidecarPath = await this.ctx.session.dumpLlmRequestToTmpDir();
101
+ } catch (error: unknown) {
102
+ sidecarError = error instanceof Error ? error.message : "Unknown error";
103
+ }
104
+ const doc = sidecarPath
105
+ ? `${formatted}\n\n---\nLLM request JSON: ${sidecarPath}\nThis file persists on disk and may contain raw context/secrets — treat accordingly.`
106
+ : formatted;
107
+ await copyToClipboard(doc);
108
+ const statusParts = ["Session copied to clipboard"];
109
+ if (sidecarPath) statusParts.push(`LLM request JSON: ${sidecarPath}`);
110
+ if (sidecarError) statusParts.push(`LLM request JSON unavailable: ${sidecarError}`);
111
+ this.ctx.showStatus(statusParts.join("\n"));
97
112
  } catch (error: unknown) {
98
113
  this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
99
114
  }
@@ -87,6 +87,7 @@ export class EventController {
87
87
  this.#streamingReveal = new StreamingRevealController({
88
88
  getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
89
89
  getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
90
+ getProseOnlyThinking: () => this.ctx.proseOnlyThinking,
90
91
  requestRender: () => this.ctx.ui.requestRender(),
91
92
  });
92
93
  this.#toolArgsReveal = new ToolArgsRevealController({
@@ -333,6 +334,7 @@ export class EventController {
333
334
  () => this.ctx.ui.requestRender(),
334
335
  this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
335
336
  this.ctx.ui.imageBudget,
337
+ this.ctx.proseOnlyThinking,
336
338
  );
337
339
  this.ctx.streamingMessage = event.message;
338
340
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
@@ -920,6 +922,17 @@ export class EventController {
920
922
  }
921
923
  }
922
924
 
925
+ /**
926
+ * Trailing Esc hint for live maintenance loaders. While a subagent is
927
+ * focused, Esc returns to main instead of cancelling its maintenance
928
+ * (#2819), so the loader drops the hint entirely rather than advertise a
929
+ * cancel that no longer happens. Includes the leading space so the focused
930
+ * label carries no dangling whitespace.
931
+ */
932
+ #maintenanceEscHint(): string {
933
+ return this.ctx.focusedAgentId ? "" : " (esc to cancel)";
934
+ }
935
+
923
936
  async #handleAutoCompactionStart(
924
937
  event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
925
938
  ): Promise<void> {
@@ -939,12 +952,14 @@ export class EventController {
939
952
  ? "Auto-handoff"
940
953
  : event.action === "shake"
941
954
  ? "Auto-shake"
942
- : "Auto context-full maintenance";
955
+ : event.action === "snapcompact"
956
+ ? "Auto-snapcompact"
957
+ : "Auto context-full maintenance";
943
958
  this.ctx.autoCompactionLoader = new Loader(
944
959
  this.ctx.ui,
945
960
  spinner => theme.fg("accent", spinner),
946
961
  text => theme.fg("muted", text),
947
- `${reasonText}${actionLabel}(esc to cancel)`,
962
+ `${reasonText}${actionLabel}…${this.#maintenanceEscHint()}`,
948
963
  getSymbolTheme().spinnerFrames,
949
964
  );
950
965
  this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
@@ -960,13 +975,16 @@ export class EventController {
960
975
  }
961
976
  const isHandoffAction = event.action === "handoff";
962
977
  const isShakeAction = event.action === "shake";
978
+ const isSnapcompactAction = event.action === "snapcompact";
963
979
  if (event.aborted) {
964
980
  this.ctx.showStatus(
965
981
  isHandoffAction
966
982
  ? "Auto-handoff cancelled"
967
983
  : isShakeAction
968
984
  ? "Auto-shake cancelled"
969
- : "Auto context-full maintenance cancelled",
985
+ : isSnapcompactAction
986
+ ? "Auto-snapcompact cancelled"
987
+ : "Auto context-full maintenance cancelled",
970
988
  );
971
989
  } else if (isShakeAction) {
972
990
  // Shake produces no CompactionResult; rebuild on success, suppress benign skips.
@@ -1005,6 +1023,8 @@ export class EventController {
1005
1023
  } else if (event.skipped) {
1006
1024
  // Benign skip: no model selected, no candidate models available, or nothing
1007
1025
  // to compact yet. Not a failure — suppress the warning.
1026
+ } else if (isSnapcompactAction) {
1027
+ this.ctx.showWarning("Auto-snapcompact maintenance failed; continuing without maintenance");
1008
1028
  } else {
1009
1029
  this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
1010
1030
  }
@@ -1027,7 +1047,7 @@ export class EventController {
1027
1047
  this.ctx.ui,
1028
1048
  spinner => theme.fg("warning", spinner),
1029
1049
  text => theme.fg("muted", text),
1030
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s(esc to cancel)`,
1050
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s…${this.#maintenanceEscHint()}`,
1031
1051
  getSymbolTheme().spinnerFrames,
1032
1052
  );
1033
1053
  this.ctx.statusContainer.addChild(this.ctx.retryLoader);
@@ -99,6 +99,7 @@ export class InputController {
99
99
  #enhancedPaste?: EnhancedPasteController;
100
100
  #focusedLeftTapListenerInstalled = false;
101
101
  #btwBranchListenerInstalled = false;
102
+ #btwCopyListenerInstalled = false;
102
103
  // Tap counter for the double-← gesture; reset whenever a quiet gap
103
104
  // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
104
105
  // #detectLeftDoubleTap.
@@ -169,11 +170,23 @@ export class InputController {
169
170
  this.ctx.ui.addInputListener(data => {
170
171
  if (!matchesKey(data, "b")) return undefined;
171
172
  if (!this.ctx.canBranchBtw()) return undefined;
173
+ if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
172
174
  if (this.ctx.editor.getText().trim()) return undefined;
173
175
  void this.ctx.handleBtwBranchKey();
174
176
  return { consume: true };
175
177
  });
176
178
  }
179
+ if (!this.#btwCopyListenerInstalled) {
180
+ this.#btwCopyListenerInstalled = true;
181
+ this.ctx.ui.addInputListener(data => {
182
+ if (!matchesKey(data, "c")) return undefined;
183
+ if (!this.ctx.canCopyBtw()) return undefined;
184
+ if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
185
+ if (this.ctx.editor.getText().trim()) return undefined;
186
+ void this.ctx.handleBtwCopyKey();
187
+ return { consume: true };
188
+ });
189
+ }
177
190
  this.ctx.editor.onEscape = () => {
178
191
  // Active context maintenance owns Esc: auto/manual compaction,
179
192
  // handoff generation, and auto-retry backoff all advertise
@@ -182,27 +195,37 @@ export class InputController {
182
195
  // to clobber the single saved-handler slot (auto-compaction start
183
196
  // → /compact → auto end → manual finally), leaving Esc wired to a
184
197
  // stale no-op closure until restart.
185
- const viewSession = this.ctx.viewSession;
186
- let aborted = false;
187
- if (viewSession.isCompacting) {
188
- try {
189
- viewSession.abortCompaction();
190
- } catch {}
191
- aborted = true;
192
- }
193
- if (viewSession.isGeneratingHandoff) {
194
- try {
195
- viewSession.abortHandoff();
196
- } catch {}
197
- aborted = true;
198
- }
199
- if (viewSession.isRetrying) {
200
- try {
201
- viewSession.abortRetry();
202
- } catch {}
203
- aborted = true;
198
+ //
199
+ // While a subagent is focused, Esc honors the advertised view action
200
+ // ("Esc returns to main") instead of cancelling maintenance —
201
+ // accidentally killing a focused subagent's compaction on the way out
202
+ // was #2819. The auto-maintenance loaders relabel their hint to match
203
+ // (see EventController). Main-session maintenance still owns Esc and
204
+ // stays cancellable from the main view (focused submit gates /compact
205
+ // and handoff, so manual maintenance is main-only anyway).
206
+ if (!this.ctx.focusedAgentId) {
207
+ const viewSession = this.ctx.viewSession;
208
+ let aborted = false;
209
+ if (viewSession.isCompacting) {
210
+ try {
211
+ viewSession.abortCompaction();
212
+ } catch {}
213
+ aborted = true;
214
+ }
215
+ if (viewSession.isGeneratingHandoff) {
216
+ try {
217
+ viewSession.abortHandoff();
218
+ } catch {}
219
+ aborted = true;
220
+ }
221
+ if (viewSession.isRetrying) {
222
+ try {
223
+ viewSession.abortRetry();
224
+ } catch {}
225
+ aborted = true;
226
+ }
227
+ if (aborted) return;
204
228
  }
205
- if (aborted) return;
206
229
 
207
230
  if (this.ctx.loopModeEnabled) {
208
231
  this.ctx.pauseLoop();
@@ -1524,7 +1547,6 @@ export class InputController {
1524
1547
  toggleThinkingBlockVisibility(): void {
1525
1548
  this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
1526
1549
  this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
1527
- this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
1528
1550
 
1529
1551
  for (const child of this.ctx.chatContainer.children) {
1530
1552
  if (child instanceof AssistantMessageComponent) {
@@ -8,7 +8,7 @@ import { type Component, replaceTabs, Spacer, Text } from "@oh-my-pi/pi-tui";
8
8
  import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
9
9
  import type { SourceMeta } from "../../capability/types";
10
10
  import { expandEnvVarsDeep } from "../../discovery/helpers";
11
- import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
11
+ import { analyzeAuthError, discoverOAuthEndpoints, loadAllMCPConfigs, MCPManager } from "../../mcp";
12
12
  import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
13
13
  import {
14
14
  addMCPServer,
@@ -1383,7 +1383,7 @@ export class MCPCommandController {
1383
1383
  }
1384
1384
  await setServerDisabled(userConfigPath, name, !enabled);
1385
1385
  if (enabled) {
1386
- await this.#reloadMCP();
1386
+ await this.#connectEnabledMCPServer(name);
1387
1387
  const state = await this.#waitForServerConnectionWithAnimation(name);
1388
1388
  const status =
1389
1389
  state === "connected"
@@ -1419,7 +1419,12 @@ export class MCPCommandController {
1419
1419
 
1420
1420
  const updated: MCPServerConfig = { ...found.config, enabled };
1421
1421
  await updateMCPServer(found.filePath, name, updated);
1422
- await this.#reloadMCP();
1422
+ if (enabled) {
1423
+ await this.#connectEnabledMCPServer(name);
1424
+ } else {
1425
+ await this.ctx.mcpManager?.disconnectServer(name);
1426
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
1427
+ }
1423
1428
 
1424
1429
  let status = "";
1425
1430
  if (enabled) {
@@ -1671,6 +1676,37 @@ export class MCPCommandController {
1671
1676
  }
1672
1677
  }
1673
1678
 
1679
+ async #connectEnabledMCPServer(name: string): Promise<void> {
1680
+ if (!this.ctx.mcpManager) {
1681
+ return;
1682
+ }
1683
+
1684
+ const { configs, sources } = await loadAllMCPConfigs(getProjectDir());
1685
+ const config = configs[name];
1686
+ if (!config) {
1687
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1688
+ return;
1689
+ }
1690
+
1691
+ const source = sources[name];
1692
+ const result = await this.ctx.mcpManager.connectServers({ [name]: config }, source ? { [name]: source } : {});
1693
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1694
+ this.#showMCPConnectionErrors(result.errors);
1695
+ }
1696
+
1697
+ #showMCPConnectionErrors(errors: Map<string, string>): void {
1698
+ if (errors.size === 0) {
1699
+ return;
1700
+ }
1701
+
1702
+ const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
1703
+ for (const [serverName, error] of errors.entries()) {
1704
+ errorLines.push(` ${serverName}: ${error}`);
1705
+ }
1706
+ errorLines.push("");
1707
+ this.#showMessage(errorLines.join("\n"));
1708
+ }
1709
+
1674
1710
  /**
1675
1711
  * Reload MCP manager with new configs
1676
1712
  */
@@ -1686,15 +1722,7 @@ export class MCPCommandController {
1686
1722
  const result = await this.ctx.mcpManager.discoverAndConnect();
1687
1723
  await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1688
1724
 
1689
- // Show any connection errors
1690
- if (result.errors.size > 0) {
1691
- const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
1692
- for (const [serverName, error] of result.errors.entries()) {
1693
- errorLines.push(` ${serverName}: ${error}`);
1694
- }
1695
- errorLines.push("");
1696
- this.#showMessage(errorLines.join("\n"));
1697
- }
1725
+ this.#showMCPConnectionErrors(result.errors);
1698
1726
  }
1699
1727
 
1700
1728
  /**
@@ -71,6 +71,10 @@ import { buildCopyTargets } from "../utils/copy-targets";
71
71
 
72
72
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
73
73
 
74
+ export function formatTemporaryModelStatus(modelLabel: string, roleSelectorHint = "Alt+M"): string {
75
+ return `Temporary model selection is session-only: ${modelLabel}. Use ${roleSelectorHint} or /model for role models (default/smol/plan/task/slow/custom roles).`;
76
+ }
77
+
74
78
  export class SelectorController {
75
79
  constructor(private ctx: InteractiveModeContext) {}
76
80
 
@@ -314,9 +318,8 @@ export class SelectorController {
314
318
  }
315
319
  }
316
320
  break;
317
- case "hideThinking":
321
+ case "hideThinkingBlock":
318
322
  this.ctx.hideThinkingBlock = value as boolean;
319
- this.ctx.session.agent.hideThinkingSummary = value as boolean;
320
323
  for (const child of this.ctx.chatContainer.children) {
321
324
  if (child instanceof AssistantMessageComponent) {
322
325
  child.setHideThinkingBlock(value as boolean);
@@ -327,6 +330,18 @@ export class SelectorController {
327
330
  // InputController.toggleThinkingBlockVisibility).
328
331
  this.ctx.ui.resetDisplay();
329
332
  break;
333
+ case "proseOnlyThinking":
334
+ this.ctx.proseOnlyThinking = value as boolean;
335
+ for (const child of this.ctx.chatContainer.children) {
336
+ if (child instanceof AssistantMessageComponent) {
337
+ child.setProseOnlyThinking(value as boolean);
338
+ }
339
+ }
340
+ this.ctx.ui.resetDisplay();
341
+ break;
342
+ case "omitThinking":
343
+ this.ctx.session.agent.hideThinkingSummary = value as boolean;
344
+ break;
330
345
  case "display.cacheMissMarker":
331
346
  // Rebuild re-runs the usage-based detection under the new setting so
332
347
  // markers appear/disappear; full reset retires any already committed
@@ -483,7 +498,8 @@ export class SelectorController {
483
498
  }
484
499
  this.ctx.statusLine.invalidate();
485
500
  this.ctx.updateEditorBorderColor();
486
- this.ctx.showStatus(`Temporary model: ${selector ?? model.id}`);
501
+ const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
502
+ this.ctx.showStatus(formatTemporaryModelStatus(selector ?? model.id, roleSelectorHint));
487
503
  done();
488
504
  this.ctx.ui.requestRender();
489
505
  } else if (role === "default") {
@@ -817,14 +833,9 @@ export class SelectorController {
817
833
  this.ctx.sessionManager.getCwd(),
818
834
  this.ctx.sessionManager.getSessionDir(),
819
835
  );
820
- // Current folder has no sessions: preload the global list so the picker
821
- // can open straight into all-projects scope instead of dead-ending.
822
- let allSessions: SessionInfo[] | undefined;
823
- let startInAllScope = false;
824
- if (sessions.length === 0) {
825
- allSessions = await SessionManager.listAll();
826
- startInAllScope = allSessions.length > 0;
827
- }
836
+ // Always open in current-folder scope; the empty-state hint in SessionList
837
+ // invites the user to Tab into all-projects rather than silently surfacing
838
+ // every project's history when the cwd has nothing to resume. See #3099.
828
839
  const historyStorage = this.ctx.historyStorage;
829
840
  const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
830
841
  this.showSelector(done => {
@@ -858,8 +869,6 @@ export class SelectorController {
858
869
  },
859
870
  historyMatcher,
860
871
  loadAllSessions: () => SessionManager.listAll(),
861
- allSessions,
862
- startInAllScope,
863
872
  getTerminalRows: () => this.ctx.ui.terminal.rows,
864
873
  },
865
874
  );
@@ -1264,6 +1273,7 @@ export class SelectorController {
1264
1273
  getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
1265
1274
  cwd: this.ctx.sessionManager.getCwd(),
1266
1275
  hideThinkingBlock: () => this.ctx.hideThinkingBlock,
1276
+ proseOnlyThinking: () => this.ctx.proseOnlyThinking,
1267
1277
  focusAgent: id => this.ctx.focusAgentSession(id),
1268
1278
  sessionFile: this.ctx.sessionManager.getSessionFile() ?? null,
1269
1279
  });