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

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
package/src/lsp/client.ts CHANGED
@@ -946,18 +946,28 @@ export async function shutdownClient(key: string): Promise<void> {
946
946
  // LSP Protocol Methods
947
947
  // =============================================================================
948
948
 
949
- /** Default timeout for LSP requests (30 seconds) */
949
+ /** Default timeout for LSP requests when no abort signal is provided (30 seconds) */
950
950
  const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
951
951
 
952
952
  /**
953
953
  * Send an LSP request and wait for response.
954
+ *
955
+ * Timeout policy:
956
+ * - If `timeoutMs` is explicitly provided, that value is used.
957
+ * - Else, if `signal` is provided, no internal timer is installed (the caller
958
+ * owns the deadline via the signal — typically a wall-clock `AbortSignal.timeout`
959
+ * from the LSP tool). Installing a second hard-coded 30s timer here used to
960
+ * cause "timed out after 30000ms" errors even when the caller had requested
961
+ * `timeout: 60`.
962
+ * - Else (no signal, no explicit timeout), fall back to `DEFAULT_REQUEST_TIMEOUT_MS`
963
+ * to avoid leaking pending requests forever.
954
964
  */
955
965
  export async function sendRequest(
956
966
  client: LspClient,
957
967
  method: string,
958
968
  params: unknown,
959
969
  signal?: AbortSignal,
960
- timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
970
+ timeoutMs?: number,
961
971
  ): Promise<unknown> {
962
972
  // Atomically increment and capture request ID
963
973
  const id = ++client.requestId;
@@ -993,15 +1003,17 @@ export async function sendRequest(
993
1003
  reject(reason);
994
1004
  };
995
1005
 
996
- // Set timeout
997
- timeout = setTimeout(() => {
998
- if (client.pendingRequests.has(id)) {
999
- client.pendingRequests.delete(id);
1000
- const err = new Error(`LSP request ${method} timed out after ${timeoutMs}ms`);
1001
- cleanup();
1002
- reject(err);
1003
- }
1004
- }, timeoutMs);
1006
+ const effectiveTimeoutMs = timeoutMs ?? (signal ? undefined : DEFAULT_REQUEST_TIMEOUT_MS);
1007
+ if (effectiveTimeoutMs !== undefined) {
1008
+ timeout = setTimeout(() => {
1009
+ if (client.pendingRequests.has(id)) {
1010
+ client.pendingRequests.delete(id);
1011
+ const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
1012
+ cleanup();
1013
+ reject(err);
1014
+ }
1015
+ }, effectiveTimeoutMs);
1016
+ }
1005
1017
  if (signal) {
1006
1018
  signal.addEventListener("abort", abortHandler, { once: true });
1007
1019
  if (signal.aborted) {
package/src/lsp/config.ts CHANGED
@@ -450,13 +450,23 @@ export function loadConfig(cwd: string): LspConfig {
450
450
  */
451
451
  export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
452
452
  const ext = path.extname(filePath).toLowerCase();
453
+ const extNoDot = ext.startsWith(".") ? ext.slice(1) : ext;
453
454
  const fileName = path.basename(filePath).toLowerCase();
454
455
  const matches: Array<[string, ServerConfig]> = [];
455
456
 
456
457
  for (const [name, serverConfig] of Object.entries(config.servers)) {
457
458
  const supportsFile = serverConfig.fileTypes.some(fileType => {
459
+ // Accept both `.ts` and `ts` forms in user config / fixtures so a
460
+ // missing dot in `fileTypes` doesn't silently exclude the server
461
+ // from extension-based routing (e.g. rename_file's relevance filter).
458
462
  const normalized = fileType.toLowerCase();
459
- return normalized === ext || normalized === fileName;
463
+ const normalizedNoDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
464
+ return (
465
+ normalized === ext ||
466
+ normalized === fileName ||
467
+ normalizedNoDot === extNoDot ||
468
+ normalizedNoDot === fileName
469
+ );
460
470
  });
461
471
 
462
472
  if (supportsFile) {
package/src/lsp/index.ts CHANGED
@@ -1261,7 +1261,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1261
1261
 
1262
1262
  // Status action doesn't need a file
1263
1263
  if (action === "status") {
1264
- const servers = Object.keys(config.servers);
1264
+ const configuredNames = Object.keys(config.servers);
1265
1265
  const lspmuxState = await detectLspmux();
1266
1266
  const lspmuxStatus = lspmuxState.available
1267
1267
  ? lspmuxState.running
@@ -1269,14 +1269,40 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1269
1269
  : "lspmux: installed but server not running"
1270
1270
  : "";
1271
1271
 
1272
- const serverStatus =
1273
- servers.length > 0
1274
- ? `Active language servers: ${servers.join(", ")}`
1275
- : "No language servers configured for this project";
1272
+ // `Object.keys(config.servers)` reflects what is *configured & resolvable
1273
+ // on PATH* — it does NOT prove the server actually starts. A wrapper
1274
+ // binary that exits immediately (e.g. rustup without the rust-analyzer
1275
+ // component) still appears here. Distinguish "configured" from
1276
+ // "started" (have a live in-process client) so callers cannot mistake
1277
+ // presence-on-PATH for a working server.
1278
+ const startedClients = getActiveClients();
1279
+ const startedByConfigName = new Map<string, LspServerStatus>();
1280
+ // getActiveClients() reports `name = client.config.command` (the
1281
+ // unresolved binary name from defaults.json), so match against
1282
+ // `serverConfig.command`, not the resolved path.
1283
+ for (const [name, serverConfig] of Object.entries(config.servers)) {
1284
+ const matched = startedClients.find(c => c.name === serverConfig.command);
1285
+ if (matched) startedByConfigName.set(name, matched);
1286
+ }
1287
+
1288
+ const lines: string[] = [];
1289
+ if (configuredNames.length === 0) {
1290
+ lines.push("No language servers configured for this project");
1291
+ } else {
1292
+ const labelled = configuredNames.map(name => {
1293
+ const started = startedByConfigName.get(name);
1294
+ if (!started) return `${name} (configured, not started)`;
1295
+ return `${name} (${started.status})`;
1296
+ });
1297
+ lines.push(`Language servers: ${labelled.join(", ")}`);
1298
+ lines.push(
1299
+ " note: 'configured, not started' means the binary resolves on PATH but no request has spawned it yet; 'ready' means a client process is live for this cwd.",
1300
+ );
1301
+ }
1302
+ if (lspmuxStatus) lines.push(lspmuxStatus);
1276
1303
 
1277
- const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
1278
1304
  return {
1279
- content: [{ type: "text", text: output }],
1305
+ content: [{ type: "text", text: lines.join("\n") }],
1280
1306
  details: { action, success: true, request: params },
1281
1307
  };
1282
1308
  }
@@ -1505,7 +1531,26 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1505
1531
  }
1506
1532
 
1507
1533
  const lspParams = { files: pairs };
1508
- const servers = getLspServers(config);
1534
+ // Filter to servers whose fileTypes match either the source or any
1535
+ // destination path. Asking every configured server about a .md/.sql/.txt
1536
+ // rename used to stack up willRenameFiles requests against irrelevant
1537
+ // language servers and hit the wall-clock timeout. A server only has
1538
+ // something useful to say about a rename if it understands one of the
1539
+ // affected file extensions.
1540
+ const allLspServers = getLspServers(config);
1541
+ const relevantNames = new Set<string>();
1542
+ const collectRelevant = (filePath: string) => {
1543
+ for (const [name] of getLspServersForFile(config, filePath)) {
1544
+ relevantNames.add(name);
1545
+ }
1546
+ };
1547
+ collectRelevant(source);
1548
+ collectRelevant(dest);
1549
+ for (const pair of pairs) {
1550
+ collectRelevant(uriToFile(pair.oldUri));
1551
+ collectRelevant(uriToFile(pair.newUri));
1552
+ }
1553
+ const servers = allLspServers.filter(([name]) => relevantNames.has(name));
1509
1554
  const respondingServers = new Set<string>();
1510
1555
  const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
1511
1556
  const serverNotes: string[] = [];
@@ -1829,8 +1874,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1829
1874
  throw new ToolAbortError();
1830
1875
  }
1831
1876
  const msg = err instanceof Error ? err.message : String(err);
1877
+ // Echo a (truncated) preview of the params we sent so the caller can
1878
+ // tell parse / shape errors (e.g. nested args dropped, missing field)
1879
+ // apart from genuine server errors without spinning up another debug call.
1880
+ const previewRaw = JSON.stringify(requestParams ?? null);
1881
+ const preview = previewRaw.length > 400 ? `${previewRaw.slice(0, 397)}...` : previewRaw;
1832
1882
  return {
1833
- content: [{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}` }],
1883
+ content: [
1884
+ { type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}\n params: ${preview}` },
1885
+ ],
1834
1886
  details: { action, serverName: chosenName, success: false, request: params },
1835
1887
  };
1836
1888
  }
package/src/lsp/utils.ts CHANGED
@@ -153,9 +153,10 @@ export function formatDiagnostic(diagnostic: Diagnostic, filePath: string): stri
153
153
  const DIAG_PATH_RE = /^(.+?):(\d+:\d+\s+.*)$/;
154
154
 
155
155
  /**
156
- * Reformat pre-formatted diagnostic messages into grep-style directory/file groups.
156
+ * Reformat pre-formatted diagnostic messages into a multi-level, prefix-folded
157
+ * directory/file grouping (see `formatGroupedFiles`).
157
158
  * Input: ["path:line:col [sev] msg", ...]
158
- * Output: "# dir/\n## file.ts\n line:col [sev] msg"
159
+ * Output: "# pkg/src/\n## file.ts\n line:col [sev] msg"
159
160
  *
160
161
  * Messages that don't match the expected format are appended ungrouped at the end.
161
162
  */
package/src/main.ts CHANGED
@@ -4,10 +4,8 @@
4
4
  * This file handles CLI argument parsing and translates them into
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
-
8
- import * as fs from "node:fs/promises";
7
+ import * as fsSync from "node:fs";
9
8
  import * as os from "node:os";
10
- import * as path from "node:path";
11
9
  import { createInterface } from "node:readline/promises";
12
10
  import { EventLoopKeepalive } from "@oh-my-pi/pi-agent-core";
13
11
  import type { ImageContent } from "@oh-my-pi/pi-ai";
@@ -28,9 +26,16 @@ import { processFileArguments } from "./cli/file-processor";
28
26
  import { buildInitialMessage } from "./cli/initial-message";
29
27
  import { runListModelsCommand } from "./cli/list-models";
30
28
  import { selectSession } from "./cli/session-picker";
29
+ import { applyStartupCwd } from "./cli/startup-cwd";
31
30
  import { findConfigFile } from "./config";
32
31
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
33
- import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
32
+ import {
33
+ getModelMatchPreferences,
34
+ resolveCliModel,
35
+ resolveModelRoleValue,
36
+ resolveModelScope,
37
+ type ScopedModel,
38
+ } from "./config/model-resolver";
34
39
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
35
40
  import { initializeWithSettings } from "./discovery";
36
41
  import {
@@ -278,7 +283,10 @@ async function runInteractiveMode(
278
283
  })
279
284
  : [];
280
285
 
281
- await mode.init({ suppressWelcomeIntro: resuming || setupScenes.length > 0 });
286
+ await mode.init({
287
+ suppressWelcomeIntro: resuming || setupScenes.length > 0,
288
+ clearInitialTerminalHistory: true,
289
+ });
282
290
 
283
291
  if (setupWizard && setupScenes.length > 0) {
284
292
  await setupWizard.runSetupWizard(mode, setupScenes);
@@ -295,12 +303,11 @@ async function runInteractiveMode(
295
303
  })
296
304
  .catch(() => {});
297
305
 
298
- // Cold-launch cleanup: wipe the terminal scrollback before painting the
299
- // resumed/new transcript. The TUI's initial paint deliberately preserves
300
- // native scrollback (prior shell content), but on `omp`/`omp -c` that leaves
301
- // the previous run's welcome + transcript stacked above the fresh one. Every
302
- // in-process session load already clears via `clearTerminalHistory`; the cold
303
- // launch is the lone path that did not.
306
+ // Cold-launch cleanup: the first paint already clears native history, and this
307
+ // replay replaces the welcome/startup frame with the resumed/new transcript.
308
+ // Every in-process session load also uses `clearTerminalHistory`; cold launch
309
+ // follows the same clean-cutover path instead of preserving a previous run's
310
+ // transcript above the fresh one.
304
311
  mode.renderInitialMessages(undefined, { preserveExistingChat: true, clearTerminalHistory: true });
305
312
 
306
313
  for (const notify of notifs) {
@@ -342,11 +349,11 @@ async function runInteractiveMode(
342
349
  }
343
350
  }
344
351
 
345
- type ForkSessionPromptResult = "accepted" | "declined" | "unavailable";
352
+ type SessionPromptResult = "accepted" | "declined" | "unavailable";
346
353
 
347
- type ForkSessionPrompt = (session: SessionInfo) => Promise<ForkSessionPromptResult>;
354
+ type SessionPrompt = (session: SessionInfo) => Promise<SessionPromptResult>;
348
355
 
349
- async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromptResult> {
356
+ async function promptForkSession(session: SessionInfo): Promise<SessionPromptResult> {
350
357
  if (!process.stdin.isTTY) {
351
358
  return "unavailable";
352
359
  }
@@ -360,6 +367,52 @@ async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromp
360
367
  }
361
368
  }
362
369
 
370
+ async function promptMoveSession(session: SessionInfo): Promise<SessionPromptResult> {
371
+ if (!process.stdin.isTTY) {
372
+ return "unavailable";
373
+ }
374
+ const message = `Session's directory no longer exists (${session.cwd}). Move (re-root) it into the current directory? [Y/n] `;
375
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
376
+ try {
377
+ const answer = (await rl.question(message)).trim().toLowerCase();
378
+ return answer === "" || answer === "y" || answer === "yes" ? "accepted" : "declined";
379
+ } finally {
380
+ rl.close();
381
+ }
382
+ }
383
+
384
+ type MissingCwdMoveResult =
385
+ | { status: "not-needed" }
386
+ | { status: "declined" }
387
+ | { status: "moved"; manager: SessionManager };
388
+
389
+ async function moveMissingCwdSessionIfNeeded(
390
+ sessionArg: string,
391
+ session: SessionInfo,
392
+ cwd: string,
393
+ sessionDir: string | undefined,
394
+ askToMoveSession: SessionPrompt,
395
+ ): Promise<MissingCwdMoveResult> {
396
+ const sourceCwd = session.cwd;
397
+ if (!sourceCwd || fsSync.existsSync(sourceCwd)) {
398
+ return { status: "not-needed" };
399
+ }
400
+
401
+ const movePromptResult = await askToMoveSession(session);
402
+ if (movePromptResult === "unavailable") {
403
+ throw new Error(
404
+ `Session "${sessionArg}" belongs to a directory that no longer exists (${sourceCwd}); run interactively to move it into the current project.`,
405
+ );
406
+ }
407
+ if (movePromptResult === "declined") {
408
+ return { status: "declined" };
409
+ }
410
+
411
+ const manager = await SessionManager.open(session.path, sessionDir);
412
+ await manager.moveTo(cwd, sessionDir);
413
+ return { status: "moved", manager };
414
+ }
415
+
363
416
  async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
364
417
  if (parsed.continue || parsed.resume) {
365
418
  return undefined;
@@ -405,7 +458,8 @@ export async function createSessionManager(
405
458
  parsed: Args,
406
459
  cwd: string,
407
460
  activeSettings: Settings = settings,
408
- askToForkSession: ForkSessionPrompt = promptForkSession,
461
+ askToForkSession: SessionPrompt = promptForkSession,
462
+ askToMoveSession: SessionPrompt = promptMoveSession,
409
463
  ): Promise<SessionManager | undefined> {
410
464
  if (parsed.fork) {
411
465
  if (parsed.noSession) {
@@ -434,10 +488,38 @@ export async function createSessionManager(
434
488
  if (!match) {
435
489
  throw new Error(`Session "${sessionArg}" not found.`);
436
490
  }
491
+ if (match.scope === "local") {
492
+ const moveResult = await moveMissingCwdSessionIfNeeded(
493
+ sessionArg,
494
+ match.session,
495
+ cwd,
496
+ parsed.sessionDir,
497
+ askToMoveSession,
498
+ );
499
+ if (moveResult.status === "moved") {
500
+ return moveResult.manager;
501
+ }
502
+ if (moveResult.status === "declined") {
503
+ return undefined;
504
+ }
505
+ }
437
506
  if (match.scope === "global") {
438
507
  const normalizedCwd = normalizePathForComparison(cwd);
439
508
  const normalizedMatchCwd = normalizePathForComparison(match.session.cwd || cwd);
440
509
  if (normalizedCwd !== normalizedMatchCwd) {
510
+ const moveResult = await moveMissingCwdSessionIfNeeded(
511
+ sessionArg,
512
+ match.session,
513
+ cwd,
514
+ parsed.sessionDir,
515
+ askToMoveSession,
516
+ );
517
+ if (moveResult.status === "moved") {
518
+ return moveResult.manager;
519
+ }
520
+ if (moveResult.status === "declined") {
521
+ return undefined;
522
+ }
441
523
  const forkPromptResult = await askToForkSession(match.session);
442
524
  if (forkPromptResult === "unavailable") {
443
525
  throw new Error(
@@ -478,56 +560,6 @@ export async function createSessionManager(
478
560
  return undefined;
479
561
  }
480
562
 
481
- async function maybeAutoChdir(parsed: Args): Promise<void> {
482
- if (parsed.allowHome || parsed.cwd) {
483
- return;
484
- }
485
-
486
- const home = os.homedir();
487
- if (!home) {
488
- return;
489
- }
490
-
491
- const normalizePath = normalizePathForComparison;
492
-
493
- const cwd = normalizePath(getProjectDir());
494
- const normalizedHome = normalizePath(home);
495
- if (cwd !== normalizedHome) {
496
- return;
497
- }
498
-
499
- const isDirectory = async (p: string) => {
500
- try {
501
- const s = await fs.stat(p);
502
- return s.isDirectory();
503
- } catch {
504
- return false;
505
- }
506
- };
507
-
508
- const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
509
- for (const candidate of candidates) {
510
- try {
511
- if (!(await isDirectory(candidate))) {
512
- continue;
513
- }
514
- setProjectDir(candidate);
515
- return;
516
- } catch {
517
- // Try next candidate.
518
- }
519
- }
520
-
521
- try {
522
- const fallback = os.tmpdir();
523
- if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
524
- setProjectDir(fallback);
525
- }
526
- } catch {
527
- // Ignore fallback errors.
528
- }
529
- }
530
-
531
563
  /** Discover SYSTEM.md file if no CLI system prompt was provided */
532
564
  function discoverSystemPromptFile(): string | undefined {
533
565
  // Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
@@ -584,9 +616,7 @@ async function buildSessionOptions(
584
616
  // Model from CLI
585
617
  // - supports --provider <name> --model <pattern>
586
618
  // - supports --model <provider>/<pattern>
587
- const modelMatchPreferences = {
588
- usageOrder: activeSettings.getStorage()?.getModelUsageOrder(),
589
- };
619
+ const modelMatchPreferences = getModelMatchPreferences(activeSettings);
590
620
  if (parsed.model) {
591
621
  const resolved = resolveCliModel({
592
622
  cliProvider: parsed.provider,
@@ -743,7 +773,7 @@ export async function runRootCommand(
743
773
  await logger.time("initTheme:initial", initTheme);
744
774
 
745
775
  const parsedArgs = parsed;
746
- await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
776
+ await logger.time("applyStartupCwd", applyStartupCwd, parsedArgs);
747
777
 
748
778
  const notifs: (InteractiveModeNotify | null)[] = [];
749
779
 
@@ -878,9 +908,7 @@ export async function runRootCommand(
878
908
 
879
909
  let scopedModels: ScopedModel[] = [];
880
910
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
881
- const modelMatchPreferences = {
882
- usageOrder: settingsInstance.getStorage()?.getModelUsageOrder(),
883
- };
911
+ const modelMatchPreferences = getModelMatchPreferences(settingsInstance);
884
912
  if (modelPatterns && modelPatterns.length > 0) {
885
913
  scopedModels = await logger.time(
886
914
  "resolveModelScope",
@@ -220,6 +220,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
220
220
  readonly mcpToolName: string;
221
221
  /** Server name */
222
222
  readonly mcpServerName: string;
223
+ readonly approval = "write" as const;
223
224
  /** Render completed MCP calls with the result header replacing the pending call header. */
224
225
  readonly mergeCallAndResult = true;
225
226
 
@@ -305,6 +306,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
305
306
  readonly mcpToolName: string;
306
307
  /** Server name */
307
308
  readonly mcpServerName: string;
309
+ readonly approval = "write" as const;
308
310
  /** Render completed MCP calls with the result header replacing the pending call header. */
309
311
  readonly mergeCallAndResult = true;
310
312
 
@@ -3,10 +3,11 @@ import type * as fsNode from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
- import { clampThinkingLevelForModel, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
6
+ import { type ApiKey, clampThinkingLevelForModel, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
7
7
  import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
+
8
9
  import type { ModelRegistry } from "../config/model-registry";
9
- import { resolveModelRoleValue } from "../config/model-resolver";
10
+ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
10
11
  import type { Settings } from "../config/settings";
11
12
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
12
13
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -271,7 +272,10 @@ async function runPhase1(options: {
271
272
  const result = await runStage1Job({
272
273
  claim,
273
274
  model: phase1Model,
274
- apiKey: phase1ApiKey,
275
+ apiKey: modelRegistry.resolver(phase1Model.provider, {
276
+ sessionId: session.sessionId,
277
+ baseUrl: phase1Model.baseUrl,
278
+ }),
275
279
  modelMaxTokens: computeModelTokenBudget(phase1Model, config),
276
280
  config,
277
281
  metadata: session.agent?.metadataForProvider(phase1Model.provider),
@@ -428,7 +432,10 @@ async function runPhase2(options: {
428
432
  const consolidated = await runConsolidationModel({
429
433
  memoryRoot,
430
434
  model: phase2Model,
431
- apiKey: phase2ApiKey,
435
+ apiKey: modelRegistry.resolver(phase2Model.provider, {
436
+ sessionId: session.sessionId,
437
+ baseUrl: phase2Model.baseUrl,
438
+ }),
432
439
  metadata: session.agent?.metadataForProvider(phase2Model.provider),
433
440
  });
434
441
  await applyConsolidation(memoryRoot, consolidated);
@@ -574,7 +581,7 @@ function extractPersistableMessages(payload: string): AgentMessage[] {
574
581
  async function runStage1Job(options: {
575
582
  claim: Stage1Claim;
576
583
  model: Model;
577
- apiKey: string;
584
+ apiKey: ApiKey;
578
585
  modelMaxTokens: number;
579
586
  config: MemoryRuntimeConfig;
580
587
  metadata?: Record<string, unknown>;
@@ -718,7 +725,7 @@ async function readRolloutSummaries(memoryRoot: string): Promise<string> {
718
725
  async function runConsolidationModel(options: {
719
726
  memoryRoot: string;
720
727
  model: Model;
721
- apiKey: string;
728
+ apiKey: ApiKey;
722
729
  metadata?: Record<string, unknown>;
723
730
  }): Promise<{
724
731
  memoryMd: string;
@@ -1081,7 +1088,7 @@ async function resolveMemoryModel(options: {
1081
1088
  if (requestedModel) {
1082
1089
  const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
1083
1090
  settings: session.settings,
1084
- matchPreferences: { usageOrder: session.settings.getStorage()?.getModelUsageOrder() },
1091
+ matchPreferences: getModelMatchPreferences(session.settings),
1085
1092
  modelRegistry,
1086
1093
  });
1087
1094
  if (resolved.model) return resolved.model;
@@ -5,6 +5,7 @@ import { Mnemopi } from "@oh-my-pi/pi-mnemopi";
5
5
  import { BankManager } from "@oh-my-pi/pi-mnemopi/core";
6
6
  import { type DiagnosticSummary, inspectDatabase } from "@oh-my-pi/pi-mnemopi/diagnose";
7
7
  import { logger } from "@oh-my-pi/pi-utils";
8
+
8
9
  import type { ModelRegistry } from "../config/model-registry";
9
10
  import { resolveRoleSelection } from "../config/model-resolver";
10
11
  import type { MemoryBackend, MemoryBackendStartOptions } from "../memory-backend/types";
@@ -334,7 +335,10 @@ async function resolveMnemopiProviderOptions(
334
335
  messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
335
336
  },
336
337
  {
337
- apiKey,
338
+ apiKey: modelRegistry.resolver(model.provider, {
339
+ sessionId,
340
+ baseUrl: model.baseUrl,
341
+ }),
338
342
  maxTokens: opts?.maxTokens,
339
343
  temperature: opts?.temperature,
340
344
  },
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs/promises";
1
2
  import * as path from "node:path";
2
3
  import {
3
4
  type Agent,
@@ -62,7 +63,7 @@ import { MCPManager } from "../../mcp/manager";
62
63
  import type { MCPServerConfig } from "../../mcp/types";
63
64
  import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
64
65
  import { theme } from "../../modes/theme/theme";
65
- import { type PlanApprovalDetails, renameApprovedPlanFile, resolvePlanTitle } from "../../plan-mode/approved-plan";
66
+ import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
66
67
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
67
68
  import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
68
69
  import {
@@ -1425,24 +1426,16 @@ export class AcpAgent implements Agent {
1425
1426
  if (!state?.enabled) {
1426
1427
  throw new ToolError("Plan mode is not active.");
1427
1428
  }
1428
- const planFilePath = state.planFilePath;
1429
- const planContent = await this.#readAcpPlanFile(session, planFilePath);
1430
- if (planContent === null) {
1431
- throw new ToolError(
1432
- `Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
1433
- );
1434
- }
1435
- const normalized = resolvePlanTitle({
1429
+ const { planFilePath, planContent, title } = await resolveApprovedPlan({
1436
1430
  suppliedTitle: extra?.title,
1437
- planContent,
1438
- planFilePath,
1431
+ statePlanFilePath: state.planFilePath,
1432
+ readPlan: url => this.#readAcpPlanFile(session, url),
1433
+ listPlanFiles: () => this.#listAcpLocalPlanFiles(session),
1439
1434
  });
1440
- const finalPlanFilePath = `local://${normalized.fileName}`;
1441
- const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, normalized.title, planContent);
1435
+ const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, title, planContent);
1442
1436
  const details: PlanApprovalDetails = {
1443
1437
  planFilePath,
1444
- finalPlanFilePath,
1445
- title: normalized.title,
1438
+ title,
1446
1439
  planExists: true,
1447
1440
  };
1448
1441
  if (!approved) {
@@ -1458,16 +1451,10 @@ export class AcpAgent implements Agent {
1458
1451
  details,
1459
1452
  };
1460
1453
  }
1461
- // Approved. Rename plan to its titled filename, set the plan
1462
- // reference so the next turn injects the plan content as
1463
- // context, then exit plan mode so the agent regains full tools.
1464
- await renameApprovedPlanFile({
1465
- planFilePath,
1466
- finalPlanFilePath,
1467
- getArtifactsDir: () => session.sessionManager.getArtifactsDir(),
1468
- getSessionId: () => session.sessionManager.getSessionId(),
1469
- });
1470
- session.setPlanReferencePath(finalPlanFilePath);
1454
+ // Approved. Set the plan reference so the next turn injects the plan
1455
+ // content as context (the file keeps its agent-chosen name no
1456
+ // rename), then exit plan mode so the agent regains full tools.
1457
+ session.setPlanReferencePath(planFilePath);
1471
1458
  session.setStandingResolveHandler?.(null);
1472
1459
  session.setPlanModeState(undefined);
1473
1460
  try {
@@ -1486,7 +1473,7 @@ export class AcpAgent implements Agent {
1486
1473
  content: [
1487
1474
  {
1488
1475
  type: "text" as const,
1489
- text: `Plan approved at ${finalPlanFilePath}. Plan mode exited; proceed with the implementation.`,
1476
+ text: `Plan approved at ${planFilePath}. Plan mode exited; proceed with the implementation.`,
1490
1477
  },
1491
1478
  ],
1492
1479
  details,
@@ -1518,6 +1505,26 @@ export class AcpAgent implements Agent {
1518
1505
  }
1519
1506
  }
1520
1507
 
1508
+ /** `local://` URLs of plan files in the session-local root, newest first —
1509
+ * the `resolveApprovedPlan` fallback for a dropped `extra.title`. */
1510
+ async #listAcpLocalPlanFiles(session: AgentSession): Promise<string[]> {
1511
+ const localRoot = this.#resolveAcpPlanFilePath(session, "local://");
1512
+ try {
1513
+ const entries = await fs.readdir(localRoot, { withFileTypes: true });
1514
+ const plans = await Promise.all(
1515
+ entries
1516
+ .filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
1517
+ .map(async entry => {
1518
+ const stat = await fs.stat(path.join(localRoot, entry.name)).catch(() => null);
1519
+ return { url: `local://${entry.name}`, mtime: stat?.mtimeMs ?? 0 };
1520
+ }),
1521
+ );
1522
+ return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
1523
+ } catch {
1524
+ return [];
1525
+ }
1526
+ }
1527
+
1521
1528
  /**
1522
1529
  * Ask the ACP client to confirm plan approval. Returns `true` only on an
1523
1530
  * explicit `APPROVE_OPTION` selection. Refine, dismissal (`undefined`), or