@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.0

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 (128) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +6 -1
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +32 -6
  22. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  23. package/dist/types/lsp/types.d.ts +10 -0
  24. package/dist/types/main.d.ts +3 -2
  25. package/dist/types/memory-backend/index.d.ts +2 -1
  26. package/dist/types/memory-backend/resolve.d.ts +1 -1
  27. package/dist/types/memory-backend/types.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  29. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/index.d.ts +5 -4
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/setup-version.d.ts +11 -0
  34. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  36. package/dist/types/modes/types.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/task/executor.d.ts +7 -0
  39. package/dist/types/telemetry-export.d.ts +1 -1
  40. package/dist/types/tools/eval-render.d.ts +1 -8
  41. package/dist/types/tools/fetch.d.ts +15 -7
  42. package/dist/types/tools/render-utils.d.ts +8 -0
  43. package/dist/types/tools/renderers.d.ts +16 -2
  44. package/dist/types/tools/search.d.ts +1 -1
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/web/scrapers/github.d.ts +22 -0
  47. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  48. package/dist/types/web/search/types.d.ts +1 -1
  49. package/package.json +9 -9
  50. package/scripts/dev-launch +42 -0
  51. package/scripts/dev-launch-preload.ts +19 -0
  52. package/src/cli/args.ts +2 -2
  53. package/src/cli/gallery-cli.ts +223 -0
  54. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  56. package/src/cli/gallery-fixtures/edit.ts +194 -0
  57. package/src/cli/gallery-fixtures/fs.ts +153 -0
  58. package/src/cli/gallery-fixtures/index.ts +40 -0
  59. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  60. package/src/cli/gallery-fixtures/memory.ts +81 -0
  61. package/src/cli/gallery-fixtures/misc.ts +221 -0
  62. package/src/cli/gallery-fixtures/search.ts +213 -0
  63. package/src/cli/gallery-fixtures/shell.ts +167 -0
  64. package/src/cli/gallery-fixtures/types.ts +41 -0
  65. package/src/cli/gallery-fixtures/web.ts +158 -0
  66. package/src/cli/gallery-screenshot.ts +279 -0
  67. package/src/cli-commands.ts +1 -0
  68. package/src/commands/gallery.ts +52 -0
  69. package/src/commands/launch.ts +1 -1
  70. package/src/config/keybindings.ts +15 -6
  71. package/src/config/model-equivalence.ts +35 -12
  72. package/src/config/model-id-affixes.ts +39 -22
  73. package/src/config/model-registry.ts +16 -16
  74. package/src/config/settings-schema.ts +18 -5
  75. package/src/config/settings.ts +11 -0
  76. package/src/dap/client.ts +14 -16
  77. package/src/edit/renderer.ts +36 -48
  78. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  79. package/src/eval/agent-bridge.ts +34 -7
  80. package/src/extensibility/extensions/runner.ts +1 -0
  81. package/src/extensibility/plugins/doctor.ts +0 -1
  82. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  83. package/src/goals/tools/goal-tool.ts +2 -2
  84. package/src/internal-urls/docs-index.generated.ts +5 -5
  85. package/src/lsp/client.ts +104 -55
  86. package/src/lsp/types.ts +10 -0
  87. package/src/main.ts +44 -49
  88. package/src/memory-backend/index.ts +13 -1
  89. package/src/memory-backend/resolve.ts +3 -5
  90. package/src/memory-backend/types.ts +1 -1
  91. package/src/modes/components/custom-editor.ts +10 -1
  92. package/src/modes/components/status-line.ts +3 -5
  93. package/src/modes/components/tool-execution.ts +61 -16
  94. package/src/modes/controllers/command-controller.ts +13 -2
  95. package/src/modes/controllers/input-controller.ts +11 -3
  96. package/src/modes/controllers/selector-controller.ts +2 -2
  97. package/src/modes/index.ts +5 -4
  98. package/src/modes/interactive-mode.ts +17 -3
  99. package/src/modes/setup-version.ts +11 -0
  100. package/src/modes/setup-wizard/index.ts +3 -2
  101. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/context-usage.ts +10 -6
  104. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  105. package/src/sdk.ts +21 -23
  106. package/src/session/agent-session.ts +7 -7
  107. package/src/slash-commands/builtin-registry.ts +1 -1
  108. package/src/slash-commands/helpers/usage-report.ts +2 -0
  109. package/src/task/executor.ts +20 -2
  110. package/src/task/render.ts +1 -2
  111. package/src/telemetry-export.ts +25 -7
  112. package/src/tools/eval-backends.ts +6 -17
  113. package/src/tools/eval-render.ts +21 -18
  114. package/src/tools/eval.ts +5 -4
  115. package/src/tools/fetch.ts +94 -84
  116. package/src/tools/render-utils.ts +17 -3
  117. package/src/tools/renderers.ts +16 -1
  118. package/src/tools/report-tool-issue.ts +1 -1
  119. package/src/tools/search.ts +173 -81
  120. package/src/tools/todo.ts +20 -7
  121. package/src/tools/write.ts +22 -1
  122. package/src/web/scrapers/github.ts +255 -3
  123. package/src/web/scrapers/youtube.ts +3 -2
  124. package/src/web/search/providers/perplexity.ts +199 -51
  125. package/src/web/search/render.ts +39 -54
  126. package/src/web/search/types.ts +5 -1
  127. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  128. package/src/eval/__tests__/shared-executors.test.ts +0 -609
package/src/lsp/client.ts CHANGED
@@ -174,49 +174,69 @@ const CLIENT_CAPABILITIES = {
174
174
  // LSP Message Protocol
175
175
  // =============================================================================
176
176
 
177
- /**
178
- * Parse a single LSP message from a buffer.
179
- * Returns the parsed message and remaining buffer, or null if incomplete.
180
- */
181
- function parseMessage(
182
- buffer: Buffer,
183
- ): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Buffer } | null {
184
- // Only decode enough to find the header
185
- const headerEndIndex = findHeaderEnd(buffer);
186
- if (headerEndIndex === -1) return null;
187
-
188
- const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
189
- const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
190
- if (!contentLengthMatch) return null;
191
-
192
- const contentLength = Number.parseInt(contentLengthMatch[1], 10);
193
- const messageStart = headerEndIndex + 4; // Skip \r\n\r\n
194
- const messageEnd = messageStart + contentLength;
195
-
196
- if (buffer.length < messageEnd) return null;
197
-
198
- const messageBytes = buffer.subarray(messageStart, messageEnd);
199
- const messageText = new TextDecoder().decode(messageBytes);
200
- const remaining = buffer.subarray(messageEnd);
201
-
202
- return {
203
- message: JSON.parse(messageText),
204
- remaining,
205
- };
206
- }
177
+ // Reused for all full (non-streaming) decodes; each decode() resets state, so a
178
+ // single instance is safe and avoids per-message TextDecoder allocation.
179
+ const MESSAGE_DECODER = new TextDecoder("utf-8");
207
180
 
208
181
  /**
209
- * Find the end of the header section (before \r\n\r\n)
182
+ * Locate the `\r\n\r\n` header terminator across the pending chunk list.
183
+ * Returns the absolute byte index of the first `\r`, or -1 when not present.
184
+ * Equivalent to scanning the contiguous concatenation of the chunks.
210
185
  */
211
- function findHeaderEnd(buffer: Uint8Array): number {
212
- for (let i = 0; i < buffer.length - 3; i++) {
213
- if (buffer[i] === 13 && buffer[i + 1] === 10 && buffer[i + 2] === 13 && buffer[i + 3] === 10) {
214
- return i;
186
+ function findHeaderEndInChunks(chunks: Buffer[]): number {
187
+ let global = 0;
188
+ let b0 = -1;
189
+ let b1 = -1;
190
+ let b2 = -1;
191
+ for (const chunk of chunks) {
192
+ for (let i = 0; i < chunk.length; i++) {
193
+ const b3 = chunk[i];
194
+ if (b0 === 13 && b1 === 10 && b2 === 13 && b3 === 10) {
195
+ return global - 3;
196
+ }
197
+ b0 = b1;
198
+ b1 = b2;
199
+ b2 = b3;
200
+ global++;
215
201
  }
216
202
  }
217
203
  return -1;
218
204
  }
219
205
 
206
+ /** Copy the byte range [from, to) out of the pending chunk list into one Buffer. */
207
+ function copyChunkRange(chunks: Buffer[], from: number, to: number): Buffer {
208
+ const out = Buffer.allocUnsafe(to - from);
209
+ let global = 0;
210
+ let written = 0;
211
+ for (const chunk of chunks) {
212
+ const chunkEnd = global + chunk.length;
213
+ if (chunkEnd > from && global < to) {
214
+ const start = Math.max(from, global) - global;
215
+ const end = Math.min(to, chunkEnd) - global;
216
+ chunk.copy(out, written, start, end);
217
+ written += end - start;
218
+ }
219
+ global = chunkEnd;
220
+ if (global >= to) break;
221
+ }
222
+ return out;
223
+ }
224
+
225
+ /** Drop the first `count` bytes from the pending chunk list in place. */
226
+ function dropChunkFront(chunks: Buffer[], count: number): void {
227
+ let removed = 0;
228
+ while (chunks.length > 0) {
229
+ const head = chunks[0];
230
+ if (removed + head.length <= count) {
231
+ removed += head.length;
232
+ chunks.shift();
233
+ } else {
234
+ chunks[0] = head.subarray(count - removed);
235
+ break;
236
+ }
237
+ }
238
+ }
239
+
220
240
  async function writeMessage(
221
241
  sink: Bun.FileSink,
222
242
  message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
@@ -249,22 +269,43 @@ async function startMessageReader(client: LspClient): Promise<void> {
249
269
 
250
270
  const reader = (client.proc.stdout as ReadableStream<Uint8Array>).getReader();
251
271
 
272
+ // Incoming bytes are buffered as a list of chunks and only joined when a full
273
+ // message is framed. Concatenating the accumulator on every read was O(n^2)
274
+ // for messages that span many reads (e.g. a large initial diagnostics burst).
275
+ const pendingChunks: Buffer[] = [];
276
+ let pendingLen = 0;
277
+ if (client.messageBuffer.length > 0) {
278
+ const seed = Buffer.from(client.messageBuffer);
279
+ pendingChunks.push(seed);
280
+ pendingLen = seed.length;
281
+ }
282
+
252
283
  try {
253
284
  while (true) {
254
285
  const { done, value } = await reader.read();
255
286
  if (done) break;
256
287
 
257
- // Atomically update buffer before processing
258
- const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, value]);
259
- client.messageBuffer = currentBuffer;
288
+ pendingChunks.push(Buffer.from(value));
289
+ pendingLen += value.length;
260
290
 
261
- // Process all complete messages in buffer
262
- // Use local variable to avoid race with concurrent buffer updates
263
- let workingBuffer = currentBuffer;
264
- let parsed = parseMessage(workingBuffer);
265
- while (parsed) {
266
- const { message, remaining } = parsed;
267
- workingBuffer = remaining;
291
+ // Drain every complete message currently buffered.
292
+ while (true) {
293
+ const headerEnd = findHeaderEndInChunks(pendingChunks);
294
+ if (headerEnd === -1) break;
295
+
296
+ const headerText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, 0, headerEnd));
297
+ const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
298
+ if (!contentLengthMatch) break;
299
+
300
+ const contentLength = Number.parseInt(contentLengthMatch[1], 10);
301
+ const messageStart = headerEnd + 4; // Skip \r\n\r\n
302
+ const messageEnd = messageStart + contentLength;
303
+ if (pendingLen < messageEnd) break;
304
+
305
+ const messageText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, messageStart, messageEnd));
306
+ const message: LspJsonRpcResponse | LspJsonRpcNotification = JSON.parse(messageText);
307
+ dropChunkFront(pendingChunks, messageEnd);
308
+ pendingLen -= messageEnd;
268
309
 
269
310
  // Route message
270
311
  if ("id" in message && message.id !== undefined) {
@@ -301,12 +342,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
301
342
  }
302
343
  }
303
344
  }
304
-
305
- parsed = parseMessage(workingBuffer);
306
345
  }
307
-
308
- // Atomically commit processed buffer
309
- client.messageBuffer = workingBuffer;
310
346
  }
311
347
  } catch (err) {
312
348
  // Connection closed or error - reject all pending requests
@@ -315,6 +351,13 @@ async function startMessageReader(client: LspClient): Promise<void> {
315
351
  }
316
352
  client.pendingRequests.clear();
317
353
  } finally {
354
+ // Persist any unparsed remainder so a restarted reader resumes mid-message.
355
+ client.messageBuffer =
356
+ pendingChunks.length === 0
357
+ ? new Uint8Array(0)
358
+ : pendingChunks.length === 1
359
+ ? pendingChunks[0]
360
+ : Buffer.concat(pendingChunks, pendingLen);
318
361
  reader.releaseLock();
319
362
  client.isReading = false;
320
363
  }
@@ -438,6 +481,7 @@ export const WARMUP_TIMEOUT_MS = 5000;
438
481
  const RUST_ANALYZER_WORKSPACE_READY_TIMEOUT_MS = 5_000;
439
482
  const RUST_ANALYZER_WORKSPACE_READY_POLL_MS = 100;
440
483
  const RUST_ANALYZER_WORKSPACE_READY_SETTLE_MS = 2_000;
484
+ const RUST_ANALYZER_STATUS_REQUEST_TIMEOUT_MS = 1_000;
441
485
  const rustAnalyzerReadyClients = new WeakSet<LspClient>();
442
486
 
443
487
  function commandBasename(command: string): string {
@@ -462,29 +506,34 @@ async function waitForRustAnalyzerWorkspace(client: LspClient, signal?: AbortSig
462
506
  if (rustAnalyzerReadyClients.has(client)) {
463
507
  return;
464
508
  }
509
+ const timings = client.config.workspaceReadyTimings;
510
+ const timeoutMs = timings?.timeoutMs ?? RUST_ANALYZER_WORKSPACE_READY_TIMEOUT_MS;
511
+ const pollMs = timings?.pollMs ?? RUST_ANALYZER_WORKSPACE_READY_POLL_MS;
512
+ const settleMs = timings?.settleMs ?? RUST_ANALYZER_WORKSPACE_READY_SETTLE_MS;
513
+ const statusRequestTimeoutMs = timings?.statusRequestTimeoutMs ?? RUST_ANALYZER_STATUS_REQUEST_TIMEOUT_MS;
465
514
  const started = Date.now();
466
- const deadline = started + RUST_ANALYZER_WORKSPACE_READY_TIMEOUT_MS;
515
+ const deadline = started + timeoutMs;
467
516
  while (true) {
468
517
  throwIfAborted(signal);
469
518
  let status: unknown;
470
519
  try {
471
- status = await sendRequest(client, "rust-analyzer/analyzerStatus", {}, signal, 1_000);
520
+ status = await sendRequest(client, "rust-analyzer/analyzerStatus", {}, signal, statusRequestTimeoutMs);
472
521
  } catch (err) {
473
522
  if (!isRustAnalyzerStatusTimeout(err) || Date.now() >= deadline) {
474
523
  return;
475
524
  }
476
- await Bun.sleep(RUST_ANALYZER_WORKSPACE_READY_POLL_MS);
525
+ await Bun.sleep(pollMs);
477
526
  continue;
478
527
  }
479
528
  const ready = typeof status === "string" && !status.startsWith("No workspaces");
480
- if (ready && Date.now() - started >= RUST_ANALYZER_WORKSPACE_READY_SETTLE_MS) {
529
+ if (ready && Date.now() - started >= settleMs) {
481
530
  rustAnalyzerReadyClients.add(client);
482
531
  return;
483
532
  }
484
533
  if (Date.now() >= deadline) {
485
534
  return;
486
535
  }
487
- await Bun.sleep(RUST_ANALYZER_WORKSPACE_READY_POLL_MS);
536
+ await Bun.sleep(pollMs);
488
537
  }
489
538
  }
490
539
 
package/src/lsp/types.ts CHANGED
@@ -356,6 +356,16 @@ export interface ServerConfig {
356
356
  disabled?: boolean;
357
357
  /** Per-server warmup timeout in milliseconds. Overrides the global WARMUP_TIMEOUT_MS for this server during startup. */
358
358
  warmupTimeoutMs?: number;
359
+ /**
360
+ * Per-server overrides for rust-analyzer workspace-ready polling. When omitted, the module
361
+ * defaults are used. Primarily a tuning/test seam to bound the multi-second settle window.
362
+ */
363
+ workspaceReadyTimings?: {
364
+ timeoutMs?: number;
365
+ pollMs?: number;
366
+ settleMs?: number;
367
+ statusRequestTimeoutMs?: number;
368
+ };
359
369
  capabilities?: ServerCapabilities;
360
370
  /** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */
361
371
  isLinter?: boolean;
package/src/main.ts CHANGED
@@ -40,19 +40,13 @@ import {
40
40
  resolveActiveProjectRegistryPath,
41
41
  } from "./discovery/helpers";
42
42
  import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
43
- import { exportFromFile } from "./export/html";
44
43
  import { ExtensionRunner } from "./extensibility/extensions/runner";
45
44
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
46
- import {
47
- getInstalledPluginsRegistryPath,
48
- getMarketplacesCacheDir,
49
- getMarketplacesRegistryPath,
50
- getPluginsCacheDir,
51
- MarketplaceManager,
52
- } from "./extensibility/plugins/marketplace";
45
+ import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
53
46
  import type { MCPManager } from "./mcp";
54
- import { InteractiveMode, runAcpMode, runPrintMode, runRpcMode } from "./modes";
55
- import { ALL_SCENES, runSetupWizard, selectSetupScenes } from "./modes/setup-wizard";
47
+ import { InteractiveMode } from "./modes/interactive-mode";
48
+ import type { PrintModeOptions } from "./modes/print-mode";
49
+ import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
56
50
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
57
51
  import type { SubmittedUserInput } from "./modes/types";
58
52
  import {
@@ -72,6 +66,13 @@ import type { LspStartupServerInfo } from "./tools";
72
66
  import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
73
67
  import { EventBus } from "./utils/event-bus";
74
68
 
69
+ type RunAcpMode = (createSession: AcpSessionFactory) => Promise<never>;
70
+ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promise<void>;
71
+ type RunRpcMode = (
72
+ session: AgentSession,
73
+ setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
74
+ ) => Promise<never>;
75
+
75
76
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
76
77
  if (!settings.get("startup.checkUpdate")) {
77
78
  return;
@@ -261,17 +262,26 @@ async function runInteractiveMode(
261
262
  eventBus,
262
263
  );
263
264
 
264
- const setupScenes = await selectSetupScenes(settings.get("setupVersion"), ALL_SCENES, mode, {
265
- resuming,
266
- isTTY: process.stdin.isTTY && process.stdout.isTTY,
267
- setupWizardEnabled: settings.get("startup.setupWizard"),
268
- force: forceSetupWizard,
269
- });
265
+ // Cold-launch gate: the full setup wizard (every scene + the overlay and
266
+ // their TUI/OAuth/search/theme deps) is heavy, yet the common case only needs
267
+ // to know whether the stored setup version is current. Lazy-load the wizard
268
+ // barrel only when setup is stale or forced; otherwise skip it entirely.
269
+ const storedSetupVersion = settings.get("setupVersion");
270
+ const setupWizard =
271
+ forceSetupWizard || storedSetupVersion < CURRENT_SETUP_VERSION ? await import("./modes/setup-wizard") : undefined;
272
+ const setupScenes = setupWizard
273
+ ? await setupWizard.selectSetupScenes(storedSetupVersion, setupWizard.ALL_SCENES, mode, {
274
+ resuming,
275
+ isTTY: process.stdin.isTTY && process.stdout.isTTY,
276
+ setupWizardEnabled: settings.get("startup.setupWizard"),
277
+ force: forceSetupWizard,
278
+ })
279
+ : [];
270
280
 
271
281
  await mode.init({ suppressWelcomeIntro: resuming || setupScenes.length > 0 });
272
282
 
273
- if (setupScenes.length > 0) {
274
- await runSetupWizard(mode, setupScenes);
283
+ if (setupWizard && setupScenes.length > 0) {
284
+ await setupWizard.runSetupWizard(mode, setupScenes);
275
285
  }
276
286
 
277
287
  versionCheckPromise
@@ -716,7 +726,7 @@ async function buildSessionOptions(
716
726
  interface RunRootCommandDependencies {
717
727
  createAgentSession?: typeof createAgentSession;
718
728
  discoverAuthStorage?: typeof discoverAuthStorage;
719
- runAcpMode?: typeof runAcpMode;
729
+ runAcpMode?: RunAcpMode;
720
730
  settings?: Settings;
721
731
  forceSetupWizard?: boolean;
722
732
  }
@@ -773,6 +783,7 @@ export async function runRootCommand(
773
783
  let result: string;
774
784
  try {
775
785
  const outputPath = parsedArgs.messages.length > 0 ? parsedArgs.messages[0] : undefined;
786
+ const { exportFromFile } = await import("./export/html");
776
787
  result = await exportFromFile(parsedArgs.export, outputPath);
777
788
  } catch (error: unknown) {
778
789
  const message = error instanceof Error ? error.message : "Failed to export session";
@@ -940,33 +951,11 @@ export async function runRootCommand(
940
951
 
941
952
  await pluginPreloadPromise;
942
953
 
943
- // Background marketplace auto-update — never blocks startup.
944
- const autoUpdate = settingsInstance.get("marketplace.autoUpdate");
945
- if (autoUpdate !== "off") {
946
- void (async () => {
947
- try {
948
- const mgr = new MarketplaceManager({
949
- marketplacesRegistryPath: getMarketplacesRegistryPath(),
950
- installedRegistryPath: getInstalledPluginsRegistryPath(),
951
- projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
952
- marketplacesCacheDir: getMarketplacesCacheDir(),
953
- pluginsCacheDir: getPluginsCacheDir(),
954
- clearPluginRootsCache: clearPluginRootsAndCaches,
955
- });
956
- await mgr.refreshStaleMarketplaces();
957
- const updates = await mgr.checkForUpdates();
958
- if (updates.length === 0) return;
959
- if (autoUpdate === "auto") {
960
- await mgr.upgradeAllPlugins();
961
- logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
962
- } else {
963
- logger.debug(`${updates.length} marketplace plugin update(s) available — /marketplace upgrade`);
964
- }
965
- } catch {
966
- // Silently ignore — network failure, corrupt data, offline.
967
- }
968
- })();
969
- }
954
+ scheduleMarketplaceAutoUpdate({
955
+ autoUpdate: settingsInstance.get("marketplace.autoUpdate"),
956
+ resolveActiveProjectRegistryPath,
957
+ clearPluginRootsCache: clearPluginRootsAndCaches,
958
+ });
970
959
 
971
960
  const { options: sessionOptions } = await logger.time(
972
961
  "buildSessionOptions",
@@ -988,7 +977,7 @@ export async function runRootCommand(
988
977
  // Both are no-ops when OTEL_EXPORTER_OTLP_ENDPOINT is unset. An empty config
989
978
  // is enough to enable telemetry — content capture is governed by the
990
979
  // standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
991
- initTelemetryExport();
980
+ await initTelemetryExport();
992
981
  if (isTelemetryExportEnabled()) {
993
982
  sessionOptions.telemetry = {};
994
983
  }
@@ -1027,7 +1016,9 @@ export async function runRootCommand(
1027
1016
  rawArgs,
1028
1017
  createSession,
1029
1018
  });
1030
- await (deps.runAcpMode ?? runAcpMode)(createAcpSession);
1019
+ // Branch-only protocol runner: keep ACP server code out of normal interactive startup.
1020
+ const runAcpMode = deps.runAcpMode ?? (await import("./modes/acp/acp-mode")).runAcpMode;
1021
+ await runAcpMode(createAcpSession);
1031
1022
  } else {
1032
1023
  // Resolve extension-registered CLI flags before creating the session so a
1033
1024
  // bad `@file` fails fast WITHOUT leaving a junk session/breadcrumb
@@ -1091,6 +1082,8 @@ export async function runRootCommand(
1091
1082
  }
1092
1083
 
1093
1084
  if (mode === "rpc" || mode === "rpc-ui") {
1085
+ // Branch-only protocol runner: keep RPC host code out of normal interactive startup.
1086
+ const runRpcMode: RunRpcMode = (await import("./modes/rpc/rpc-mode")).runRpcMode;
1094
1087
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
1095
1088
  } else if (isInteractive) {
1096
1089
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
@@ -1109,7 +1102,7 @@ export async function runRootCommand(
1109
1102
 
1110
1103
  if ($env.PI_TIMING) {
1111
1104
  logger.printTimings();
1112
- if ($env.PI_TIMING === "x") {
1105
+ if (logger.shouldExitAfterTimings()) {
1113
1106
  process.exit(0);
1114
1107
  }
1115
1108
  }
@@ -1132,6 +1125,8 @@ export async function runRootCommand(
1132
1125
  initialImages,
1133
1126
  );
1134
1127
  } else {
1128
+ // Branch-only single-shot runner: keep print-mode code out of normal interactive startup.
1129
+ const runPrintMode: RunPrintMode = (await import("./modes/print-mode")).runPrintMode;
1135
1130
  await runPrintMode(session, {
1136
1131
  mode,
1137
1132
  messages: initialArgs.messages,
@@ -1,4 +1,16 @@
1
- export * from "../mnemopi";
1
+ export type {
2
+ MnemopiBackendConfig,
3
+ MnemopiLlmMode,
4
+ MnemopiProviderOptions,
5
+ MnemopiScoping,
6
+ } from "../mnemopi/config";
7
+ export type {
8
+ MnemopiMemoryEditOperation,
9
+ MnemopiMemoryEditOptions,
10
+ MnemopiMemoryEditResult,
11
+ MnemopiSessionState,
12
+ MnemopiSessionStateOptions,
13
+ } from "../mnemopi/state";
2
14
  export * from "./local-backend";
3
15
  export * from "./off-backend";
4
16
  export * from "./resolve";
@@ -1,6 +1,4 @@
1
1
  import type { Settings } from "../config/settings";
2
- import { hindsightBackend } from "../hindsight";
3
- import { mnemopiBackend } from "../mnemopi";
4
2
  import { localBackend } from "./local-backend";
5
3
  import { offBackend } from "./off-backend";
6
4
  import type { MemoryBackend } from "./types";
@@ -18,10 +16,10 @@ import type { MemoryBackend } from "./types";
18
16
  * `memories.enabled` remains accepted only as a legacy migration input. Once
19
17
  * a config is loaded, `memory.backend` is the sole runtime selector.
20
18
  */
21
- export function resolveMemoryBackend(settings: Settings): MemoryBackend {
19
+ export async function resolveMemoryBackend(settings: Settings): Promise<MemoryBackend> {
22
20
  const id = settings.get("memory.backend");
23
- if (id === "hindsight") return hindsightBackend;
24
- if (id === "mnemopi") return mnemopiBackend;
21
+ if (id === "hindsight") return (await import("../hindsight/backend")).hindsightBackend;
22
+ if (id === "mnemopi") return (await import("../mnemopi/backend")).mnemopiBackend;
25
23
  if (id === "local") return localBackend;
26
24
  return offBackend;
27
25
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Memory backend abstraction.
3
3
  *
4
- * Backends are mutually exclusive — `resolveMemoryBackend(settings)` returns
4
+ * Backends are mutually exclusive — `await resolveMemoryBackend(settings)` resolves
5
5
  * exactly one. Implementations MUST be self-contained: they own the per-session
6
6
  * state they create in `start()` and tear it down on `clear()`.
7
7
  */
@@ -10,6 +10,7 @@ type ConfigurableEditorAction = Extract<
10
10
  | "app.clear"
11
11
  | "app.exit"
12
12
  | "app.suspend"
13
+ | "app.display.reset"
13
14
  | "app.thinking.cycle"
14
15
  | "app.model.cycleForward"
15
16
  | "app.model.cycleBackward"
@@ -30,10 +31,11 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
30
31
  "app.clear": ["ctrl+c"],
31
32
  "app.exit": ["ctrl+d"],
32
33
  "app.suspend": ["ctrl+z"],
34
+ "app.display.reset": ["ctrl+l"],
33
35
  "app.thinking.cycle": ["shift+tab"],
34
36
  "app.model.cycleForward": ["ctrl+p"],
35
37
  "app.model.cycleBackward": ["shift+ctrl+p"],
36
- "app.model.select": ["ctrl+l"],
38
+ "app.model.select": ["alt+m"],
37
39
  "app.model.selectTemporary": ["alt+p"],
38
40
  "app.tools.expand": ["ctrl+o"],
39
41
  "app.thinking.toggle": ["ctrl+t"],
@@ -65,6 +67,7 @@ export class CustomEditor extends Editor {
65
67
  onEscape?: () => void;
66
68
  onClear?: () => void;
67
69
  onExit?: () => void;
70
+ onDisplayReset?: () => void;
68
71
  onCycleThinkingLevel?: () => void;
69
72
  onCycleModelForward?: () => void;
70
73
  onCycleModelBackward?: () => void;
@@ -158,6 +161,12 @@ export class CustomEditor extends Editor {
158
161
  return;
159
162
  }
160
163
 
164
+ // Intercept configured display reset shortcut
165
+ if (this.#matchesAction(data, "app.display.reset") && this.onDisplayReset) {
166
+ this.onDisplayReset();
167
+ return;
168
+ }
169
+
161
170
  // Intercept configured suspend shortcut
162
171
  if (this.#matchesAction(data, "app.suspend") && this.onSuspend) {
163
172
  this.onSuspend();
@@ -546,7 +546,7 @@ export class StatusLineComponent implements Component {
546
546
  return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
547
547
  }
548
548
 
549
- #buildSegmentContext(width: number): SegmentContext {
549
+ #buildSegmentContext(width: number, segmentOptions: StatusLineSettings["segmentOptions"]): SegmentContext {
550
550
  const state = this.session.state;
551
551
 
552
552
  // Trigger background fetch (5-min TTL); render uses cached value
@@ -575,7 +575,7 @@ export class StatusLineComponent implements Component {
575
575
  return {
576
576
  session: this.session,
577
577
  width,
578
- options: this.#resolveSettings().segmentOptions ?? {},
578
+ options: segmentOptions ?? {},
579
579
  planMode: this.#planModeStatus,
580
580
  loopMode: this.#loopModeStatus,
581
581
  goalMode: this.#goalModeStatus,
@@ -632,8 +632,8 @@ export class StatusLineComponent implements Component {
632
632
  }
633
633
 
634
634
  #buildStatusLine(width: number): string {
635
- const ctx = this.#buildSegmentContext(width);
636
635
  const effectiveSettings = this.#resolveSettings();
636
+ const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions);
637
637
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
638
638
 
639
639
  const bgAnsi = theme.getBgAnsi("statusLineBg");
@@ -759,8 +759,6 @@ export class StatusLineComponent implements Component {
759
759
  return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
760
760
  }
761
761
 
762
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
763
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
764
762
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
765
763
  const sessionName =
766
764
  effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;