@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3

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 (154) hide show
  1. package/CHANGELOG.md +113 -1
  2. package/dist/types/cli/gallery-fixtures/types.d.ts +7 -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/config/keybindings.d.ts +2 -2
  6. package/dist/types/config/model-provider-priority.d.ts +1 -0
  7. package/dist/types/config/model-resolver.d.ts +4 -1
  8. package/dist/types/config/settings.d.ts +7 -2
  9. package/dist/types/debug/report-bundle.d.ts +3 -0
  10. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  11. package/dist/types/edit/index.d.ts +0 -1
  12. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  14. package/dist/types/lsp/client.d.ts +10 -0
  15. package/dist/types/lsp/index.d.ts +0 -5
  16. package/dist/types/main.d.ts +14 -9
  17. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  18. package/dist/types/modes/components/assistant-message.d.ts +0 -9
  19. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  20. package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
  21. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  22. package/dist/types/modes/components/session-selector.d.ts +16 -7
  23. package/dist/types/modes/components/status-line.d.ts +2 -0
  24. package/dist/types/modes/components/tool-execution.d.ts +0 -18
  25. package/dist/types/modes/controllers/event-controller.d.ts +17 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -0
  27. package/dist/types/modes/magic-keywords.d.ts +1 -1
  28. package/dist/types/modes/markdown-prose.d.ts +1 -1
  29. package/dist/types/modes/types.d.ts +7 -0
  30. package/dist/types/modes/workflow.d.ts +3 -3
  31. package/dist/types/session/auth-storage.d.ts +1 -1
  32. package/dist/types/session/messages.d.ts +11 -8
  33. package/dist/types/session/session-manager.d.ts +5 -2
  34. package/dist/types/session/yield-queue.d.ts +10 -1
  35. package/dist/types/task/executor.d.ts +10 -0
  36. package/dist/types/tools/eval-render.d.ts +0 -1
  37. package/dist/types/tools/eval.d.ts +8 -0
  38. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  39. package/dist/types/tools/github-cache.d.ts +12 -0
  40. package/dist/types/tools/index.d.ts +31 -0
  41. package/dist/types/tools/path-utils.d.ts +13 -1
  42. package/dist/types/tools/read.d.ts +2 -1
  43. package/dist/types/tools/render-utils.d.ts +3 -1
  44. package/dist/types/tools/renderers.d.ts +0 -15
  45. package/dist/types/tools/search.d.ts +2 -2
  46. package/dist/types/tools/write.d.ts +0 -2
  47. package/dist/types/tools/yield.d.ts +8 -0
  48. package/dist/types/tui/code-cell.d.ts +0 -2
  49. package/dist/types/tui/hyperlink.d.ts +5 -7
  50. package/dist/types/tui/output-block.d.ts +0 -18
  51. package/package.json +9 -9
  52. package/src/cli/args.ts +3 -1
  53. package/src/cli/dry-balance-cli.ts +2 -4
  54. package/src/cli/gallery-cli.ts +4 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +0 -1
  56. package/src/cli/gallery-fixtures/fs.ts +68 -1
  57. package/src/cli/gallery-fixtures/types.ts +8 -1
  58. package/src/cli/startup-cwd.ts +68 -0
  59. package/src/commands/launch.ts +3 -0
  60. package/src/commit/agentic/agent.ts +1 -0
  61. package/src/commit/model-selection.ts +3 -2
  62. package/src/config/model-provider-priority.ts +55 -0
  63. package/src/config/model-registry.ts +4 -22
  64. package/src/config/model-resolver.ts +39 -7
  65. package/src/config/settings.ts +86 -41
  66. package/src/debug/index.ts +8 -0
  67. package/src/debug/raw-sse-buffer.ts +7 -4
  68. package/src/debug/report-bundle.ts +9 -0
  69. package/src/edit/file-snapshot-store.ts +33 -1
  70. package/src/edit/hashline/diff.ts +86 -0
  71. package/src/edit/hashline/execute.ts +14 -1
  72. package/src/edit/hashline/filesystem.ts +2 -1
  73. package/src/edit/index.ts +31 -17
  74. package/src/edit/renderer.ts +116 -31
  75. package/src/eval/__tests__/llm-bridge.test.ts +20 -0
  76. package/src/eval/js/context-manager.ts +32 -15
  77. package/src/eval/js/shared/prelude.txt +26 -10
  78. package/src/eval/llm-bridge.ts +14 -3
  79. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  80. package/src/eval/py/executor.ts +23 -11
  81. package/src/eval/py/prelude.py +1 -1
  82. package/src/extensibility/extensions/types.ts +10 -1
  83. package/src/internal-urls/docs-index.generated.ts +7 -7
  84. package/src/lsp/client.ts +23 -11
  85. package/src/lsp/config.ts +11 -1
  86. package/src/lsp/index.ts +189 -61
  87. package/src/main.ts +144 -78
  88. package/src/mcp/tool-bridge.ts +2 -0
  89. package/src/memories/index.ts +2 -2
  90. package/src/modes/components/assistant-message.ts +3 -15
  91. package/src/modes/components/custom-editor.ts +143 -111
  92. package/src/modes/components/late-diagnostics-message.ts +60 -0
  93. package/src/modes/components/model-selector.ts +59 -13
  94. package/src/modes/components/oauth-selector.ts +33 -7
  95. package/src/modes/components/plan-review-overlay.ts +26 -5
  96. package/src/modes/components/read-tool-group.ts +415 -35
  97. package/src/modes/components/session-selector.ts +89 -35
  98. package/src/modes/components/status-line.ts +19 -4
  99. package/src/modes/components/tips.txt +1 -1
  100. package/src/modes/components/tool-execution.ts +7 -49
  101. package/src/modes/components/transcript-container.ts +108 -32
  102. package/src/modes/components/user-message.ts +1 -1
  103. package/src/modes/controllers/event-controller.ts +32 -1
  104. package/src/modes/controllers/input-controller.ts +56 -9
  105. package/src/modes/interactive-mode.ts +107 -20
  106. package/src/modes/magic-keywords.ts +1 -1
  107. package/src/modes/markdown-prose.ts +1 -1
  108. package/src/modes/theme/shimmer.ts +20 -9
  109. package/src/modes/types.ts +7 -0
  110. package/src/modes/utils/ui-helpers.ts +26 -5
  111. package/src/modes/workflow.ts +10 -10
  112. package/src/prompts/system/manual-continue.md +7 -0
  113. package/src/prompts/system/plan-mode-active.md +56 -72
  114. package/src/prompts/system/workflow-notice.md +1 -1
  115. package/src/prompts/tools/bash.md +9 -0
  116. package/src/prompts/tools/browser.md +1 -1
  117. package/src/prompts/tools/eval.md +5 -2
  118. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  119. package/src/prompts/tools/read.md +2 -2
  120. package/src/sdk.ts +85 -10
  121. package/src/session/agent-session.ts +42 -15
  122. package/src/session/auth-storage.ts +2 -0
  123. package/src/session/messages.ts +21 -14
  124. package/src/session/session-manager.ts +98 -25
  125. package/src/session/yield-queue.ts +20 -2
  126. package/src/task/executor.ts +72 -36
  127. package/src/task/render.ts +3 -4
  128. package/src/tiny/title-client.ts +6 -1
  129. package/src/tools/bash.ts +7 -7
  130. package/src/tools/browser/tab-supervisor.ts +13 -1
  131. package/src/tools/browser/tab-worker.ts +33 -4
  132. package/src/tools/eval-render.ts +4 -23
  133. package/src/tools/eval.ts +13 -2
  134. package/src/tools/find.ts +148 -99
  135. package/src/tools/gh-cache-invalidation.ts +200 -0
  136. package/src/tools/github-cache.ts +25 -0
  137. package/src/tools/index.ts +32 -0
  138. package/src/tools/inspect-image.ts +2 -2
  139. package/src/tools/path-utils.ts +47 -24
  140. package/src/tools/plan-mode-guard.ts +52 -7
  141. package/src/tools/read.ts +41 -20
  142. package/src/tools/render-utils.ts +3 -1
  143. package/src/tools/renderers.ts +0 -15
  144. package/src/tools/search.ts +38 -3
  145. package/src/tools/ssh.ts +0 -1
  146. package/src/tools/todo.ts +1 -0
  147. package/src/tools/write.ts +5 -14
  148. package/src/tools/yield.ts +10 -1
  149. package/src/tui/code-cell.ts +1 -6
  150. package/src/tui/hyperlink.ts +13 -23
  151. package/src/tui/output-block.ts +2 -97
  152. package/src/utils/commit-message-generator.ts +2 -2
  153. package/src/utils/enhanced-paste.ts +30 -2
  154. package/src/web/search/providers/codex.ts +37 -8
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 {
@@ -166,7 +171,8 @@ export async function submitInteractiveInput(
166
171
 
167
172
  try {
168
173
  using _keepalive = new EventLoopKeepalive();
169
- // Continue shortcuts submit an already-started empty prompt with no optimistic user message.
174
+ // Continue shortcuts submit an already-started synthetic developer prompt with
175
+ // no optimistic user message.
170
176
  if (!input.started && !mode.markPendingSubmissionStarted(input)) {
171
177
  return;
172
178
  }
@@ -177,6 +183,8 @@ export async function submitInteractiveInput(
177
183
  display: input.display ?? false,
178
184
  attribution: "agent",
179
185
  });
186
+ } else if (input.synthetic) {
187
+ await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
180
188
  } else {
181
189
  await session.prompt(input.text, { images: input.images });
182
190
  }
@@ -344,11 +352,11 @@ async function runInteractiveMode(
344
352
  }
345
353
  }
346
354
 
347
- type ForkSessionPromptResult = "accepted" | "declined" | "unavailable";
355
+ type SessionPromptResult = "accepted" | "declined" | "unavailable";
348
356
 
349
- type ForkSessionPrompt = (session: SessionInfo) => Promise<ForkSessionPromptResult>;
357
+ type SessionPrompt = (session: SessionInfo) => Promise<SessionPromptResult>;
350
358
 
351
- async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromptResult> {
359
+ async function promptForkSession(session: SessionInfo): Promise<SessionPromptResult> {
352
360
  if (!process.stdin.isTTY) {
353
361
  return "unavailable";
354
362
  }
@@ -362,6 +370,68 @@ async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromp
362
370
  }
363
371
  }
364
372
 
373
+ async function promptMoveSession(session: SessionInfo): Promise<SessionPromptResult> {
374
+ if (!process.stdin.isTTY) {
375
+ return "unavailable";
376
+ }
377
+ const message = `Session's directory no longer exists (${session.cwd}). Move (re-root) it into the current directory? [Y/n] `;
378
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
379
+ try {
380
+ const answer = (await rl.question(message)).trim().toLowerCase();
381
+ return answer === "" || answer === "y" || answer === "yes" ? "accepted" : "declined";
382
+ } finally {
383
+ rl.close();
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Friendly CLI failure raised by {@link createSessionManager} when the user's
389
+ * session-resolution flags (`--resume`/`--fork`/cross-project prompts) cannot
390
+ * be satisfied. {@link runRootCommand} catches it and prints a clean stderr
391
+ * message instead of letting it surface as `[Uncaught Exception]`
392
+ * (see issue #2084).
393
+ */
394
+ export class SessionResolutionError extends Error {
395
+ readonly hint?: string;
396
+ constructor(message: string, hint?: string) {
397
+ super(message);
398
+ this.name = "SessionResolutionError";
399
+ this.hint = hint;
400
+ }
401
+ }
402
+
403
+ type MissingCwdMoveResult =
404
+ | { status: "not-needed" }
405
+ | { status: "declined" }
406
+ | { status: "moved"; manager: SessionManager };
407
+
408
+ async function moveMissingCwdSessionIfNeeded(
409
+ sessionArg: string,
410
+ session: SessionInfo,
411
+ cwd: string,
412
+ sessionDir: string | undefined,
413
+ askToMoveSession: SessionPrompt,
414
+ ): Promise<MissingCwdMoveResult> {
415
+ const sourceCwd = session.cwd;
416
+ if (!sourceCwd || fsSync.existsSync(sourceCwd)) {
417
+ return { status: "not-needed" };
418
+ }
419
+
420
+ const movePromptResult = await askToMoveSession(session);
421
+ if (movePromptResult === "unavailable") {
422
+ throw new SessionResolutionError(
423
+ `Session "${sessionArg}" belongs to a directory that no longer exists (${sourceCwd}); run interactively to move it into the current project.`,
424
+ );
425
+ }
426
+ if (movePromptResult === "declined") {
427
+ return { status: "declined" };
428
+ }
429
+
430
+ const manager = await SessionManager.open(session.path, sessionDir);
431
+ await manager.moveTo(cwd, sessionDir);
432
+ return { status: "moved", manager };
433
+ }
434
+
365
435
  async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
366
436
  if (parsed.continue || parsed.resume) {
367
437
  return undefined;
@@ -407,11 +477,12 @@ export async function createSessionManager(
407
477
  parsed: Args,
408
478
  cwd: string,
409
479
  activeSettings: Settings = settings,
410
- askToForkSession: ForkSessionPrompt = promptForkSession,
480
+ askToForkSession: SessionPrompt = promptForkSession,
481
+ askToMoveSession: SessionPrompt = promptMoveSession,
411
482
  ): Promise<SessionManager | undefined> {
412
483
  if (parsed.fork) {
413
484
  if (parsed.noSession) {
414
- throw new Error("--fork requires session persistence");
485
+ throw new SessionResolutionError("--fork requires session persistence");
415
486
  }
416
487
  const forkSource = parsed.fork;
417
488
  if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
@@ -419,7 +490,10 @@ export async function createSessionManager(
419
490
  }
420
491
  const match = await resolveResumableSession(forkSource, cwd, parsed.sessionDir);
421
492
  if (!match) {
422
- throw new Error(`Session "${forkSource}" not found.`);
493
+ throw new SessionResolutionError(
494
+ `Session "${forkSource}" not found.`,
495
+ "Run `omp --resume` without an argument to pick from recent sessions, or `omp` to start a new one.",
496
+ );
423
497
  }
424
498
  return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir);
425
499
  }
@@ -434,15 +508,46 @@ export async function createSessionManager(
434
508
  }
435
509
  const match = await resolveResumableSession(sessionArg, cwd, parsed.sessionDir);
436
510
  if (!match) {
437
- throw new Error(`Session "${sessionArg}" not found.`);
511
+ throw new SessionResolutionError(
512
+ `Session "${sessionArg}" not found.`,
513
+ "Run `omp --resume` without an argument to pick from recent sessions, or `omp` to start a new one.",
514
+ );
515
+ }
516
+ if (match.scope === "local") {
517
+ const moveResult = await moveMissingCwdSessionIfNeeded(
518
+ sessionArg,
519
+ match.session,
520
+ cwd,
521
+ parsed.sessionDir,
522
+ askToMoveSession,
523
+ );
524
+ if (moveResult.status === "moved") {
525
+ return moveResult.manager;
526
+ }
527
+ if (moveResult.status === "declined") {
528
+ return undefined;
529
+ }
438
530
  }
439
531
  if (match.scope === "global") {
440
532
  const normalizedCwd = normalizePathForComparison(cwd);
441
533
  const normalizedMatchCwd = normalizePathForComparison(match.session.cwd || cwd);
442
534
  if (normalizedCwd !== normalizedMatchCwd) {
535
+ const moveResult = await moveMissingCwdSessionIfNeeded(
536
+ sessionArg,
537
+ match.session,
538
+ cwd,
539
+ parsed.sessionDir,
540
+ askToMoveSession,
541
+ );
542
+ if (moveResult.status === "moved") {
543
+ return moveResult.manager;
544
+ }
545
+ if (moveResult.status === "declined") {
546
+ return undefined;
547
+ }
443
548
  const forkPromptResult = await askToForkSession(match.session);
444
549
  if (forkPromptResult === "unavailable") {
445
- throw new Error(
550
+ throw new SessionResolutionError(
446
551
  `Session "${sessionArg}" is in another project (${match.session.cwd}); run interactively to fork it into the current project.`,
447
552
  );
448
553
  }
@@ -480,56 +585,6 @@ export async function createSessionManager(
480
585
  return undefined;
481
586
  }
482
587
 
483
- async function maybeAutoChdir(parsed: Args): Promise<void> {
484
- if (parsed.allowHome || parsed.cwd) {
485
- return;
486
- }
487
-
488
- const home = os.homedir();
489
- if (!home) {
490
- return;
491
- }
492
-
493
- const normalizePath = normalizePathForComparison;
494
-
495
- const cwd = normalizePath(getProjectDir());
496
- const normalizedHome = normalizePath(home);
497
- if (cwd !== normalizedHome) {
498
- return;
499
- }
500
-
501
- const isDirectory = async (p: string) => {
502
- try {
503
- const s = await fs.stat(p);
504
- return s.isDirectory();
505
- } catch {
506
- return false;
507
- }
508
- };
509
-
510
- const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
511
- for (const candidate of candidates) {
512
- try {
513
- if (!(await isDirectory(candidate))) {
514
- continue;
515
- }
516
- setProjectDir(candidate);
517
- return;
518
- } catch {
519
- // Try next candidate.
520
- }
521
- }
522
-
523
- try {
524
- const fallback = os.tmpdir();
525
- if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
526
- setProjectDir(fallback);
527
- }
528
- } catch {
529
- // Ignore fallback errors.
530
- }
531
- }
532
-
533
588
  /** Discover SYSTEM.md file if no CLI system prompt was provided */
534
589
  function discoverSystemPromptFile(): string | undefined {
535
590
  // Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
@@ -586,9 +641,7 @@ async function buildSessionOptions(
586
641
  // Model from CLI
587
642
  // - supports --provider <name> --model <pattern>
588
643
  // - supports --model <provider>/<pattern>
589
- const modelMatchPreferences = {
590
- usageOrder: activeSettings.getStorage()?.getModelUsageOrder(),
591
- };
644
+ const modelMatchPreferences = getModelMatchPreferences(activeSettings);
592
645
  if (parsed.model) {
593
646
  const resolved = resolveCliModel({
594
647
  cliProvider: parsed.provider,
@@ -745,7 +798,7 @@ export async function runRootCommand(
745
798
  await logger.time("initTheme:initial", initTheme);
746
799
 
747
800
  const parsedArgs = parsed;
748
- await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
801
+ await logger.time("applyStartupCwd", applyStartupCwd, parsedArgs);
749
802
 
750
803
  const notifs: (InteractiveModeNotify | null)[] = [];
751
804
 
@@ -880,9 +933,7 @@ export async function runRootCommand(
880
933
 
881
934
  let scopedModels: ScopedModel[] = [];
882
935
  const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
883
- const modelMatchPreferences = {
884
- usageOrder: settingsInstance.getStorage()?.getModelUsageOrder(),
885
- };
936
+ const modelMatchPreferences = getModelMatchPreferences(settingsInstance);
886
937
  if (modelPatterns && modelPatterns.length > 0) {
887
938
  scopedModels = await logger.time(
888
939
  "resolveModelScope",
@@ -893,14 +944,29 @@ export async function runRootCommand(
893
944
  );
894
945
  }
895
946
 
896
- // Create session manager based on CLI flags
897
- let sessionManager = await logger.time(
898
- "createSessionManager",
899
- createSessionManager,
900
- parsedArgs,
901
- cwd,
902
- settingsInstance,
903
- );
947
+ // Create session manager based on CLI flags. SessionResolutionError signals a
948
+ // user-facing failure (unknown --resume/--fork id, non-interactive fork
949
+ // prompt, --fork with --no-session): print + exit cleanly instead of letting
950
+ // it surface as `[Uncaught Exception]` (see issue #2084).
951
+ let sessionManager: SessionManager | undefined;
952
+ try {
953
+ sessionManager = await logger.time(
954
+ "createSessionManager",
955
+ createSessionManager,
956
+ parsedArgs,
957
+ cwd,
958
+ settingsInstance,
959
+ );
960
+ } catch (error: unknown) {
961
+ if (error instanceof SessionResolutionError) {
962
+ process.stderr.write(`${chalk.red(`Error: ${error.message}`)}\n`);
963
+ if (error.hint) {
964
+ process.stderr.write(`${chalk.dim(error.hint)}\n`);
965
+ }
966
+ process.exit(1);
967
+ }
968
+ throw error;
969
+ }
904
970
 
905
971
  // User declined the cross-project fork prompt — exit cleanly with a friendly
906
972
  // message rather than letting the decline bubble up as an uncaught exception
@@ -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
 
@@ -7,7 +7,7 @@ import { type ApiKey, clampThinkingLevelForModel, completeSimple, Effort, type M
7
7
  import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
8
 
9
9
  import type { ModelRegistry } from "../config/model-registry";
10
- import { resolveModelRoleValue } from "../config/model-resolver";
10
+ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
11
11
  import type { Settings } from "../config/settings";
12
12
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
13
13
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -1088,7 +1088,7 @@ async function resolveMemoryModel(options: {
1088
1088
  if (requestedModel) {
1089
1089
  const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
1090
1090
  settings: session.settings,
1091
- matchPreferences: { usageOrder: session.settings.getStorage()?.getModelUsageOrder() },
1091
+ matchPreferences: getModelMatchPreferences(session.settings),
1092
1092
  modelRegistry,
1093
1093
  });
1094
1094
  if (resolved.model) return resolved.model;
@@ -4,7 +4,7 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
4
4
  import { settings } from "../../config/settings";
5
5
  import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
6
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
- import { isSilentAbort, resolveAbortLabel } from "../../session/messages";
7
+ import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
8
8
  import { resolveImageOptions } from "../../tools/render-utils";
9
9
 
10
10
  /**
@@ -74,18 +74,6 @@ export class AssistantMessageComponent extends Container {
74
74
  return this.#transcriptBlockFinalized;
75
75
  }
76
76
 
77
- /**
78
- * Assistant text/thinking streams in append-only: earlier rendered rows never
79
- * re-layout, new content only grows the block at the bottom. The transcript
80
- * reports this so the renderer may commit scrolled-off head rows of a long
81
- * streamed reply to native scrollback instead of dropping them (see
82
- * `NativeScrollbackLiveRegion#getNativeScrollbackCommitSafeEnd`). Volatile
83
- * blocks (tool previews that collapse) intentionally do not implement this.
84
- */
85
- isTranscriptBlockAppendOnly(): boolean {
86
- return true;
87
- }
88
-
89
77
  markTranscriptBlockFinalized(): void {
90
78
  this.#transcriptBlockFinalized = true;
91
79
  }
@@ -252,7 +240,7 @@ export class AssistantMessageComponent extends Container {
252
240
  // But only if there are no tool calls (tool execution components will show the error)
253
241
  const hasToolCalls = message.content.some(c => c.type === "toolCall");
254
242
  if (!hasToolCalls) {
255
- if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
243
+ if (message.stopReason === "aborted" && shouldRenderAbortReason(message.errorMessage)) {
256
244
  const abortMessage = resolveAbortLabel(message.errorMessage);
257
245
  if (hasVisibleContent) {
258
246
  this.#contentContainer.addChild(new Spacer(1));
@@ -268,7 +256,7 @@ export class AssistantMessageComponent extends Container {
268
256
  }
269
257
  if (
270
258
  message.errorMessage &&
271
- !isSilentAbort(message.errorMessage) &&
259
+ shouldRenderAbortReason(message.errorMessage) &&
272
260
  message.stopReason !== "aborted" &&
273
261
  message.stopReason !== "error"
274
262
  ) {