@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
@@ -1,7 +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 { canonicalizeMessage } from "../../utils/thinking-display";
4
+ import { formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
5
5
  import type { AssistantMessageComponent } from "../components/assistant-message";
6
6
 
7
7
  export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
@@ -9,11 +9,13 @@ export const MIN_STEP = 3;
9
9
  export const CATCHUP_FRAMES = 8;
10
10
 
11
11
  type AssistantContentBlock = AssistantMessage["content"][number];
12
+ type DisplayThinkingContentBlock = Extract<AssistantContentBlock, { type: "thinking" }> & { rawThinking?: string };
12
13
  type StreamingRevealComponent = Pick<AssistantMessageComponent, "updateContent">;
13
14
 
14
15
  type StreamingRevealControllerOptions = {
15
16
  getSmoothStreaming(): boolean;
16
17
  getHideThinkingBlock(): boolean;
18
+ getProseOnlyThinking(): boolean;
17
19
  requestRender(): void;
18
20
  };
19
21
 
@@ -83,13 +85,16 @@ function sliceGraphemes(text: string, units: number): string {
83
85
  return text;
84
86
  }
85
87
 
86
- export function visibleUnits(message: AssistantMessage, hideThinking: boolean): number {
88
+ export function visibleUnits(message: AssistantMessage, hideThinking: boolean, proseOnly = true): number {
87
89
  let total = 0;
88
90
  for (const block of message.content) {
89
91
  if (block.type === "text") {
90
92
  total += countGraphemes(block.text);
91
- } else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
92
- total += countGraphemes(block.thinking);
93
+ } else if (block.type === "thinking" && !hideThinking) {
94
+ const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
95
+ if (hasDisplayableThinking(block.thinking, formatted)) {
96
+ total += countGraphemes(formatted);
97
+ }
93
98
  }
94
99
  }
95
100
  return total;
@@ -119,6 +124,7 @@ export function buildDisplayMessage(
119
124
  target: AssistantMessage,
120
125
  revealed: number,
121
126
  hideThinking: boolean,
127
+ proseOnly = true,
122
128
  countOf: (index: number, text: string) => number = (_index, text) => countGraphemes(text),
123
129
  ): AssistantMessage {
124
130
  let remaining = Math.max(0, Math.floor(revealed));
@@ -129,10 +135,20 @@ export function buildDisplayMessage(
129
135
  const units = countOf(i, block.text);
130
136
  content.push(revealTextBlock(block, remaining, units));
131
137
  remaining = Math.max(0, remaining - units);
132
- } else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
133
- const units = countOf(i, block.thinking);
134
- content.push(revealThinkingBlock(block, remaining, units));
135
- remaining = Math.max(0, remaining - units);
138
+ } else if (block.type === "thinking" && !hideThinking) {
139
+ const formatted = formatThinkingForDisplay(block.thinking, proseOnly);
140
+ if (hasDisplayableThinking(block.thinking, formatted)) {
141
+ const units = countOf(i, formatted);
142
+ const displayBlock: DisplayThinkingContentBlock = {
143
+ ...block,
144
+ thinking: formatted,
145
+ rawThinking: block.thinking,
146
+ };
147
+ content.push(revealThinkingBlock(displayBlock, remaining, units));
148
+ remaining = Math.max(0, remaining - units);
149
+ } else {
150
+ content.push(block);
151
+ }
136
152
  } else {
137
153
  content.push(block);
138
154
  }
@@ -147,12 +163,14 @@ export function nextStep(backlog: number): number {
147
163
  export class StreamingRevealController {
148
164
  readonly #getSmoothStreaming: () => boolean;
149
165
  readonly #getHideThinkingBlock: () => boolean;
166
+ readonly #getProseOnlyThinking: () => boolean;
150
167
  readonly #requestRender: () => void;
151
168
  #target: AssistantMessage | undefined;
152
169
  #component: StreamingRevealComponent | undefined;
153
170
  #timer: NodeJS.Timeout | undefined;
154
171
  #revealed = 0;
155
172
  #hideThinkingBlock = false;
173
+ #proseOnlyThinking = true;
156
174
  #smoothStreaming = true;
157
175
  readonly #unitCounter = new BlockUnitCounter();
158
176
  readonly #countOf = (index: number, text: string): number => this.#unitCounter.count(index, text);
@@ -160,6 +178,7 @@ export class StreamingRevealController {
160
178
  constructor(options: StreamingRevealControllerOptions) {
161
179
  this.#getSmoothStreaming = options.getSmoothStreaming;
162
180
  this.#getHideThinkingBlock = options.getHideThinkingBlock;
181
+ this.#getProseOnlyThinking = options.getProseOnlyThinking;
163
182
  this.#requestRender = options.requestRender;
164
183
  }
165
184
 
@@ -169,9 +188,14 @@ export class StreamingRevealController {
169
188
  this.#target = message;
170
189
  this.#revealed = 0;
171
190
  this.#hideThinkingBlock = this.#getHideThinkingBlock();
191
+ this.#proseOnlyThinking = this.#getProseOnlyThinking();
172
192
  this.#smoothStreaming = this.#getSmoothStreaming();
173
193
  if (!this.#smoothStreaming) {
174
- component.updateContent(message, { transient: true });
194
+ const total = this.#visibleUnits(message);
195
+ component.updateContent(
196
+ buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
197
+ { transient: true },
198
+ );
175
199
  return;
176
200
  }
177
201
  const total = this.#visibleUnits(message);
@@ -179,9 +203,18 @@ export class StreamingRevealController {
179
203
  // A tool call is a transcript-order boundary: finish any leading
180
204
  // assistant text before EventController renders the separate tool card.
181
205
  this.#revealed = total;
182
- component.updateContent(buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf), {
183
- transient: true,
184
- });
206
+ component.updateContent(
207
+ buildDisplayMessage(
208
+ message,
209
+ this.#revealed,
210
+ this.#hideThinkingBlock,
211
+ this.#proseOnlyThinking,
212
+ this.#countOf,
213
+ ),
214
+ {
215
+ transient: true,
216
+ },
217
+ );
185
218
  return;
186
219
  }
187
220
  this.#renderCurrent();
@@ -190,9 +223,16 @@ export class StreamingRevealController {
190
223
 
191
224
  setTarget(message: AssistantMessage): void {
192
225
  this.#target = message;
226
+ this.#hideThinkingBlock = this.#getHideThinkingBlock();
227
+ this.#proseOnlyThinking = this.#getProseOnlyThinking();
228
+ this.#smoothStreaming = this.#getSmoothStreaming();
193
229
  if (!this.#component) return;
194
230
  if (!this.#smoothStreaming) {
195
- this.#component.updateContent(message, { transient: true });
231
+ const total = this.#visibleUnits(message);
232
+ this.#component.updateContent(
233
+ buildDisplayMessage(message, total, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
234
+ { transient: true },
235
+ );
196
236
  return;
197
237
  }
198
238
  const total = this.#visibleUnits(message);
@@ -202,7 +242,13 @@ export class StreamingRevealController {
202
242
  this.#revealed = total;
203
243
  this.#stopTimer();
204
244
  this.#component.updateContent(
205
- buildDisplayMessage(message, this.#revealed, this.#hideThinkingBlock, this.#countOf),
245
+ buildDisplayMessage(
246
+ message,
247
+ this.#revealed,
248
+ this.#hideThinkingBlock,
249
+ this.#proseOnlyThinking,
250
+ this.#countOf,
251
+ ),
206
252
  {
207
253
  transient: true,
208
254
  },
@@ -231,8 +277,11 @@ export class StreamingRevealController {
231
277
  const block = message.content[i]!;
232
278
  if (block.type === "text") {
233
279
  total += this.#unitCounter.count(i, block.text);
234
- } else if (block.type === "thinking" && !this.#hideThinkingBlock && canonicalizeMessage(block.thinking)) {
235
- total += this.#unitCounter.count(i, block.thinking);
280
+ } else if (block.type === "thinking" && !this.#hideThinkingBlock) {
281
+ const formatted = formatThinkingForDisplay(block.thinking, this.#proseOnlyThinking);
282
+ if (hasDisplayableThinking(block.thinking, formatted)) {
283
+ total += this.#unitCounter.count(i, formatted);
284
+ }
236
285
  }
237
286
  }
238
287
  return total;
@@ -244,7 +293,13 @@ export class StreamingRevealController {
244
293
  // smooth reveal has temporarily caught up to the current target. The
245
294
  // message_end handler performs the only stable non-transient render.
246
295
  this.#component.updateContent(
247
- buildDisplayMessage(this.#target, this.#revealed, this.#hideThinkingBlock, this.#countOf),
296
+ buildDisplayMessage(
297
+ this.#target,
298
+ this.#revealed,
299
+ this.#hideThinkingBlock,
300
+ this.#proseOnlyThinking,
301
+ this.#countOf,
302
+ ),
248
303
  { transient: true },
249
304
  );
250
305
  }
@@ -284,9 +339,12 @@ export class StreamingRevealController {
284
339
  return;
285
340
  }
286
341
  this.#revealed = Math.min(total, this.#revealed + nextStep(total - this.#revealed));
287
- component.updateContent(buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock, this.#countOf), {
288
- transient: true,
289
- });
342
+ component.updateContent(
343
+ buildDisplayMessage(target, this.#revealed, this.#hideThinkingBlock, this.#proseOnlyThinking, this.#countOf),
344
+ {
345
+ transient: true,
346
+ },
347
+ );
290
348
  this.#requestRender();
291
349
  if (this.#revealed >= total) {
292
350
  this.#stopTimer();
@@ -1,10 +1,10 @@
1
1
  import * as fs from "node:fs/promises";
2
- import { resolveToCwd } from "../../tools/path-utils";
3
2
  import {
4
3
  applyOpsToPhases,
5
4
  getLatestTodoPhasesFromEntries,
6
5
  markdownToPhases,
7
6
  phasesToMarkdown,
7
+ resolveTodoMarkdownPath,
8
8
  type TodoItem,
9
9
  type TodoPhase,
10
10
  USER_TODO_EDIT_CUSTOM_TYPE,
@@ -18,8 +18,8 @@ const USAGE = [
18
18
  " /todo Show current todos",
19
19
  " /todo edit Open todos in $EDITOR",
20
20
  " /todo copy Copy todos as Markdown to clipboard",
21
- " /todo export <path> Write todos as Markdown to <path>",
22
- " /todo import <path> Replace todos from Markdown at <path>",
21
+ " /todo export [<path>] Write todos to file (default: TODO.md)",
22
+ " /todo import [<path>] Replace todos from file (default: TODO.md)",
23
23
  " /todo append [<phase>] <task...> Append a task; phase fuzzy-matched or auto-created",
24
24
  " /todo start <task> Mark task in_progress (fuzzy content match)",
25
25
  " /todo done [<task|phase>] Mark task/phase/all completed",
@@ -214,9 +214,7 @@ export class TodoCommandController {
214
214
  }
215
215
 
216
216
  #resolveTodoPath(rest: string): string {
217
- const trimmed = rest.trim();
218
- const raw = trimmed || "TODO.md";
219
- return resolveToCwd(raw, this.ctx.sessionManager.getCwd());
217
+ return resolveTodoMarkdownPath(rest, this.ctx.sessionManager.getCwd());
220
218
  }
221
219
 
222
220
  async #exportToFile(rest: string): Promise<void> {
@@ -225,22 +223,23 @@ export class TodoCommandController {
225
223
  this.ctx.showWarning("No todos to export.");
226
224
  return;
227
225
  }
228
- const target = this.#resolveTodoPath(rest);
229
226
  try {
227
+ const target = this.#resolveTodoPath(rest);
230
228
  await fs.writeFile(target, phasesToMarkdown(phases), "utf8");
231
229
  this.ctx.showStatus(`Wrote todos to ${target}`);
232
230
  } catch (error) {
233
- this.ctx.showError(`Failed to write ${target}: ${error instanceof Error ? error.message : String(error)}`);
231
+ this.ctx.showError(`Failed to write todos: ${error instanceof Error ? error.message : String(error)}`);
234
232
  }
235
233
  }
236
234
 
237
235
  async #importFromFile(rest: string): Promise<void> {
238
- const source = this.#resolveTodoPath(rest);
236
+ let source = "";
239
237
  let content: string;
240
238
  try {
239
+ source = this.#resolveTodoPath(rest);
241
240
  content = await fs.readFile(source, "utf8");
242
241
  } catch (error) {
243
- this.ctx.showError(`Failed to read ${source}: ${error instanceof Error ? error.message : String(error)}`);
242
+ this.ctx.showError(`Failed to read todos: ${error instanceof Error ? error.message : String(error)}`);
244
243
  return;
245
244
  }
246
245
  const { phases, errors } = markdownToPhases(content);
@@ -70,7 +70,12 @@ import type { Goal, GoalModeState } from "../goals/state";
70
70
  import { resolveLocalUrlToPath } from "../internal-urls";
71
71
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
72
72
  import type { MCPManager } from "../mcp";
73
- import { formatMCPConnectingMessage, isMcpConnectingEvent, MCP_CONNECTING_EVENT_CHANNEL } from "../mcp/startup-events";
73
+ import {
74
+ formatMCPConnectionStatusMessage,
75
+ isMcpConnectionStatusEvent,
76
+ MCP_CONNECTION_STATUS_EVENT_CHANNEL,
77
+ type McpConnectionStatusEvent,
78
+ } from "../mcp/startup-events";
74
79
  import {
75
80
  humanizePlanTitle,
76
81
  type PlanApprovalDetails,
@@ -88,7 +93,7 @@ import type { SessionContext } from "../session/session-context";
88
93
  import { getRecentSessions } from "../session/session-listing";
89
94
  import type { SessionManager } from "../session/session-manager";
90
95
  import type { ShakeMode } from "../session/shake-types";
91
- import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, BUILTIN_SLASH_COMMANDS } from "../slash-commands/builtin-registry";
96
+ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } from "../slash-commands/builtin-registry";
92
97
  import { formatDuration } from "../slash-commands/helpers/format";
93
98
  import { STTController, type SttState } from "../stt";
94
99
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
@@ -401,6 +406,7 @@ export class InteractiveMode implements InteractiveModeContext {
401
406
  #modelCycleClearTimer: NodeJS.Timeout | undefined;
402
407
  todoPhases: TodoPhase[] = [];
403
408
  hideThinkingBlock = false;
409
+ proseOnlyThinking = true;
404
410
  pendingImages: ImageContent[] = [];
405
411
  pendingImageLinks: (string | undefined)[] = [];
406
412
  compactionQueuedMessages: CompactionQueuedMessage[] = [];
@@ -506,6 +512,14 @@ export class InteractiveMode implements InteractiveModeContext {
506
512
  this.loadingAnimation.stop();
507
513
  this.loadingAnimation = undefined;
508
514
  }
515
+ if (this.autoCompactionLoader) {
516
+ this.autoCompactionLoader.stop();
517
+ this.autoCompactionLoader = undefined;
518
+ }
519
+ if (this.retryLoader) {
520
+ this.retryLoader.stop();
521
+ this.retryLoader = undefined;
522
+ }
509
523
  this.statusContainer.clear();
510
524
  this.pendingMessagesContainer.clear();
511
525
  this.#cancelModelCycleClearTimer();
@@ -526,6 +540,10 @@ export class InteractiveMode implements InteractiveModeContext {
526
540
  #observerRegistry: SessionObserverRegistry;
527
541
  #eventBus?: EventBus;
528
542
  #eventBusUnsubscribers: Array<() => void> = [];
543
+ #mcpStatusOrder: string[] = [];
544
+ #mcpPendingServers = new Set<string>();
545
+ #mcpConnectedServers = new Set<string>();
546
+ #mcpFailedServers = new Map<string, string>();
529
547
  #welcomeComponent?: WelcomeComponent;
530
548
  readonly #chatHost: ChatBlockHost = { requestRender: () => this.ui.requestRender() };
531
549
 
@@ -559,13 +577,12 @@ export class InteractiveMode implements InteractiveModeContext {
559
577
  }),
560
578
  );
561
579
  this.#eventBusUnsubscribers.push(
562
- eventBus.on(MCP_CONNECTING_EVENT_CHANNEL, data => {
563
- if (!isMcpConnectingEvent(data)) {
564
- logger.warn("Ignoring malformed mcp:connecting event", { data });
580
+ eventBus.on(MCP_CONNECTION_STATUS_EVENT_CHANNEL, data => {
581
+ if (!isMcpConnectionStatusEvent(data)) {
582
+ logger.warn("Ignoring malformed mcp:connection-status event", { data });
565
583
  return;
566
584
  }
567
- if (this.settings.get("startup.quiet")) return;
568
- this.showStatus(formatMCPConnectingMessage(data.serverNames));
585
+ this.#handleMcpConnectionStatusEvent(data);
569
586
  }),
570
587
  );
571
588
  }
@@ -618,6 +635,7 @@ export class InteractiveMode implements InteractiveModeContext {
618
635
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
619
636
 
620
637
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
638
+ this.proseOnlyThinking = settings.get("proseOnlyThinking");
621
639
 
622
640
  const hookCommands: SlashCommand[] = (
623
641
  this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
@@ -643,8 +661,9 @@ export class InteractiveMode implements InteractiveModeContext {
643
661
  }
644
662
  }
645
663
 
664
+ const builtinCommands = buildTuiBuiltinSlashCommands({ ctx: this });
646
665
  // Store pending commands for init() where file commands are loaded async
647
- this.#pendingSlashCommands = [...BUILTIN_SLASH_COMMANDS, ...hookCommands, ...customCommands, ...skillCommandList];
666
+ this.#pendingSlashCommands = [...builtinCommands, ...hookCommands, ...customCommands, ...skillCommandList];
648
667
 
649
668
  this.#uiHelpers = new UiHelpers(this);
650
669
  this.#btwController = new BtwController(this);
@@ -660,6 +679,54 @@ export class InteractiveMode implements InteractiveModeContext {
660
679
  this.#observerRegistry = new SessionObserverRegistry();
661
680
  }
662
681
 
682
+ #handleMcpConnectionStatusEvent(event: McpConnectionStatusEvent): void {
683
+ if (this.settings.get("startup.quiet")) return;
684
+ if (event.type === "connecting") {
685
+ this.#mcpStatusOrder = [];
686
+ this.#mcpPendingServers.clear();
687
+ this.#mcpConnectedServers.clear();
688
+ this.#mcpFailedServers.clear();
689
+ for (const serverName of event.serverNames) {
690
+ this.#trackMcpStatusServer(serverName);
691
+ this.#mcpPendingServers.add(serverName);
692
+ }
693
+ } else if (event.type === "connected") {
694
+ this.#trackMcpStatusServer(event.serverName);
695
+ this.#mcpPendingServers.delete(event.serverName);
696
+ this.#mcpFailedServers.delete(event.serverName);
697
+ this.#mcpConnectedServers.add(event.serverName);
698
+ } else {
699
+ this.#trackMcpStatusServer(event.serverName);
700
+ this.#mcpPendingServers.delete(event.serverName);
701
+ this.#mcpConnectedServers.delete(event.serverName);
702
+ this.#mcpFailedServers.set(event.serverName, event.error);
703
+ }
704
+
705
+ const message = formatMCPConnectionStatusMessage({
706
+ pendingServers: this.#orderedMcpStatusServers(this.#mcpPendingServers),
707
+ connectedServers: this.#orderedMcpStatusServers(this.#mcpConnectedServers),
708
+ failedServers: this.#orderedMcpStatusFailures(),
709
+ });
710
+ if (message) this.showStatus(message);
711
+ }
712
+
713
+ #trackMcpStatusServer(serverName: string): void {
714
+ if (!this.#mcpStatusOrder.includes(serverName)) {
715
+ this.#mcpStatusOrder.push(serverName);
716
+ }
717
+ }
718
+
719
+ #orderedMcpStatusServers(servers: ReadonlySet<string>): string[] {
720
+ return this.#mcpStatusOrder.filter(serverName => servers.has(serverName));
721
+ }
722
+
723
+ #orderedMcpStatusFailures(): Array<{ serverName: string; error: string }> {
724
+ return this.#mcpStatusOrder.flatMap(serverName => {
725
+ const error = this.#mcpFailedServers.get(serverName);
726
+ return error === undefined ? [] : [{ serverName, error }];
727
+ });
728
+ }
729
+
663
730
  playWelcomeIntro(): void {
664
731
  const welcome = this.#welcomeComponent;
665
732
  // Component-scoped: the intro only mutates the welcome box's own rows,
@@ -3424,7 +3491,7 @@ export class InteractiveMode implements InteractiveModeContext {
3424
3491
  return this.#commandController.handleExportCommand(text);
3425
3492
  }
3426
3493
 
3427
- handleDumpCommand() {
3494
+ async handleDumpCommand(): Promise<void> {
3428
3495
  return this.#commandController.handleDumpCommand();
3429
3496
  }
3430
3497
 
@@ -3769,6 +3836,14 @@ export class InteractiveMode implements InteractiveModeContext {
3769
3836
  return this.#btwController.handleBranch();
3770
3837
  }
3771
3838
 
3839
+ canCopyBtw(): boolean {
3840
+ return this.#btwController.canCopy();
3841
+ }
3842
+
3843
+ handleBtwCopyKey(): Promise<boolean> {
3844
+ return this.#btwController.handleCopy();
3845
+ }
3846
+
3772
3847
  async handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void> {
3773
3848
  try {
3774
3849
  const result = await this.session.branchFromBtw(question, assistantMessage);
@@ -148,6 +148,7 @@ export interface InteractiveModeContext {
148
148
  loopLimit?: LoopLimitRuntime;
149
149
  planModePlanFilePath?: string;
150
150
  hideThinkingBlock: boolean;
151
+ proseOnlyThinking: boolean;
151
152
  pendingImages: ImageContent[];
152
153
  pendingImageLinks: (string | undefined)[];
153
154
  compactionQueuedMessages: CompactionQueuedMessage[];
@@ -289,7 +290,7 @@ export interface InteractiveModeContext {
289
290
  handleHotkeysCommand(): void;
290
291
  handleToolsCommand(): void;
291
292
  handleContextCommand(): void;
292
- handleDumpCommand(): void;
293
+ handleDumpCommand(): Promise<void>;
293
294
  handleAdvisorDumpCommand(isRaw?: boolean): void;
294
295
  handleDebugTranscriptCommand(): Promise<void>;
295
296
  handleClearCommand(): Promise<void>;
@@ -348,6 +349,8 @@ export interface InteractiveModeContext {
348
349
  handleBtwEscape(): boolean;
349
350
  handleBtwBranchKey(): Promise<boolean>;
350
351
  canBranchBtw(): boolean;
352
+ canCopyBtw(): boolean;
353
+ handleBtwCopyKey(): Promise<boolean>;
351
354
  handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void>;
352
355
  handleOmfgCommand(complaint: string): Promise<void>;
353
356
  hasActiveOmfg(): boolean;
@@ -332,6 +332,7 @@ export class UiHelpers {
332
332
  () => this.ctx.ui.requestRender(),
333
333
  this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
334
334
  this.ctx.ui.imageBudget,
335
+ this.ctx.proseOnlyThinking,
335
336
  );
336
337
  this.ctx.chatContainer.addChild(assistantComponent);
337
338
  break;
@@ -24,7 +24,7 @@ Keep exploration lean:
24
24
  - Offer alternatives, not lectures.
25
25
  - NEVER restate information the agent already has, including errors they have seen.
26
26
  - Examples: type errors, LSP diagnostics, failed builds, failing tests, lint.
27
- - NEVER repeat advice you already gave.
27
+ - NEVER repeat advice you already gave, and NEVER send the same advice twice.
28
28
  - NEVER nitpick about things user stated they are okay with. You are the advocate for the user.
29
29
  </communication>
30
30
 
@@ -0,0 +1,3 @@
1
+ <system-reminder>
2
+ This is an ephemeral side-channel turn that reuses the current conversation's context. The tool catalog stays attached only to keep the prompt cache warm — tools are NOT available on this turn. Do NOT emit any tool call; reply with plain text only. Any tool call you produce is discarded without executing.
3
+ </system-reminder>
package/src/sdk.ts CHANGED
@@ -87,7 +87,7 @@ import {
87
87
  type MCPToolsLoadResult,
88
88
  parseMCPToolName,
89
89
  } from "./mcp";
90
- import { MCP_CONNECTING_EVENT_CHANNEL, type McpConnectingEvent } from "./mcp/startup-events";
90
+ import { MCP_CONNECTION_STATUS_EVENT_CHANNEL, type McpConnectionStatusEvent } from "./mcp/startup-events";
91
91
  import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memory-backend";
92
92
  import type { MnemopiSessionState } from "./mnemopi/state";
93
93
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
@@ -805,6 +805,7 @@ export interface BuildSystemPromptOptions {
805
805
  * as separate entries so providers can cache prompt prefixes without concatenating blocks.
806
806
  */
807
807
  export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
808
+ const toolMap = options.tools ? new Map(options.tools.map(tool => [tool.name, tool])) : undefined;
808
809
  return await buildSystemPromptInternal({
809
810
  cwd: options.cwd,
810
811
  customPrompt: options.customPrompt,
@@ -812,6 +813,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
812
813
  contextFiles: options.contextFiles,
813
814
  appendSystemPrompt: options.appendPrompt,
814
815
  inlineToolDescriptors: options.inlineToolDescriptors,
816
+ toolNames: options.tools?.map(tool => tool.name),
817
+ tools: toolMap ? buildSystemPromptToolMetadata(toolMap) : undefined,
815
818
  });
816
819
  }
817
820
 
@@ -1617,12 +1620,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1617
1620
  | ((liveSession: AgentSession, activation: DeferredMCPActivation) => void)
1618
1621
  | undefined;
1619
1622
  const startupQuiet = settings.get("startup.quiet");
1620
- const onMCPConnecting = (serverNames: string[]) => {
1621
- if (!options.hasUI || startupQuiet || serverNames.length === 0) return;
1622
- eventBus.emit(MCP_CONNECTING_EVENT_CHANNEL, { serverNames } satisfies McpConnectingEvent);
1623
+ const onMCPStatus = (event: McpConnectionStatusEvent) => {
1624
+ if (!options.hasUI || startupQuiet) return;
1625
+ if (event.type === "connecting" && event.serverNames.length === 0) return;
1626
+ eventBus.emit(MCP_CONNECTION_STATUS_EVENT_CHANNEL, event);
1623
1627
  };
1624
1628
  const mcpDiscoverOptions = {
1625
- onConnecting: onMCPConnecting,
1629
+ onStatus: onMCPStatus,
1626
1630
  enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
1627
1631
  // Always filter Exa - we have native integration
1628
1632
  filterExa: true,
@@ -2412,9 +2416,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2412
2416
  : undefined;
2413
2417
  const transformProviderContext =
2414
2418
  obfuscator || snapcompactInline
2415
- ? (context: Context, transformModel: Model): Context => {
2419
+ ? async (context: Context, transformModel: Model): Promise<Context> => {
2416
2420
  let transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
2417
- if (snapcompactInline) transformed = snapcompactInline.transform(transformed, transformModel);
2421
+ if (snapcompactInline) transformed = await snapcompactInline.transform(transformed, transformModel);
2418
2422
  return transformed;
2419
2423
  }
2420
2424
  : undefined;
@@ -2474,7 +2478,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2474
2478
  presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
2475
2479
  repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
2476
2480
  serviceTier: initialServiceTier,
2477
- hideThinkingSummary: settings.get("hideThinkingBlock"),
2481
+ hideThinkingSummary: settings.get("omitThinking"),
2478
2482
  kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
2479
2483
  preferWebsockets: preferOpenAICodexWebsockets,
2480
2484
  getToolContext: tc => toolContextStore.getContext(tc),