@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.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 (146) hide show
  1. package/CHANGELOG.md +159 -182
  2. package/dist/types/async/job-manager.d.ts +3 -3
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/claude-trace-cli.d.ts +54 -0
  5. package/dist/types/cli/session-picker.d.ts +10 -3
  6. package/dist/types/cli/update-cli.d.ts +17 -0
  7. package/dist/types/commands/launch.d.ts +3 -0
  8. package/dist/types/config/keybindings.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +11 -2
  10. package/dist/types/config/settings.d.ts +13 -0
  11. package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
  12. package/dist/types/eval/concurrency-bridge.d.ts +26 -0
  13. package/dist/types/eval/heartbeat.d.ts +45 -0
  14. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  15. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  16. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  17. package/dist/types/extensibility/extensions/types.d.ts +24 -5
  18. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
  19. package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
  20. package/dist/types/lsp/index.d.ts +2 -0
  21. package/dist/types/lsp/utils.d.ts +4 -0
  22. package/dist/types/main.d.ts +5 -0
  23. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  24. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  25. package/dist/types/modes/components/custom-editor.d.ts +3 -2
  26. package/dist/types/modes/components/hook-selector.d.ts +10 -1
  27. package/dist/types/modes/components/session-selector.d.ts +32 -5
  28. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  29. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
  30. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +10 -3
  32. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  33. package/dist/types/modes/theme/theme.d.ts +1 -1
  34. package/dist/types/modes/types.d.ts +5 -3
  35. package/dist/types/registry/agent-registry.d.ts +1 -1
  36. package/dist/types/sdk.d.ts +2 -2
  37. package/dist/types/session/agent-session.d.ts +12 -3
  38. package/dist/types/session/history-storage.d.ts +16 -1
  39. package/dist/types/session/session-manager.d.ts +4 -0
  40. package/dist/types/task/output-manager.d.ts +6 -15
  41. package/dist/types/tools/ask.d.ts +8 -6
  42. package/dist/types/tools/eval-backends.d.ts +12 -0
  43. package/dist/types/tools/eval-render.d.ts +52 -0
  44. package/dist/types/tools/eval.d.ts +2 -35
  45. package/dist/types/tools/find.d.ts +0 -9
  46. package/dist/types/tools/index.d.ts +5 -12
  47. package/dist/types/tools/path-utils.d.ts +16 -0
  48. package/dist/types/tools/sqlite-reader.d.ts +25 -8
  49. package/dist/types/tui/output-block.d.ts +4 -3
  50. package/dist/types/utils/clipboard.d.ts +4 -0
  51. package/dist/types/web/kagi.d.ts +76 -0
  52. package/dist/types/web/search/providers/exa.d.ts +7 -1
  53. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  54. package/examples/extensions/README.md +1 -0
  55. package/examples/extensions/thinking-note.ts +13 -0
  56. package/package.json +9 -9
  57. package/src/async/job-manager.ts +3 -3
  58. package/src/cli/args.ts +6 -2
  59. package/src/cli/claude-trace-cli.ts +783 -0
  60. package/src/cli/session-picker.ts +36 -10
  61. package/src/cli/update-cli.ts +35 -2
  62. package/src/commands/launch.ts +3 -0
  63. package/src/config/keybindings.ts +6 -0
  64. package/src/config/model-registry.ts +33 -4
  65. package/src/config/settings-schema.ts +12 -2
  66. package/src/config/settings.ts +23 -0
  67. package/src/discovery/claude-plugins.ts +7 -9
  68. package/src/discovery/claude.ts +41 -22
  69. package/src/edit/index.ts +23 -3
  70. package/src/eval/__tests__/agent-bridge.test.ts +148 -4
  71. package/src/eval/__tests__/heartbeat.test.ts +84 -0
  72. package/src/eval/__tests__/llm-bridge.test.ts +30 -0
  73. package/src/eval/agent-bridge.ts +44 -38
  74. package/src/eval/concurrency-bridge.ts +34 -0
  75. package/src/eval/heartbeat.ts +74 -0
  76. package/src/eval/js/executor.ts +13 -9
  77. package/src/eval/js/shared/prelude.txt +20 -17
  78. package/src/eval/js/tool-bridge.ts +5 -0
  79. package/src/eval/llm-bridge.ts +20 -14
  80. package/src/eval/py/executor.ts +14 -18
  81. package/src/eval/py/prelude.py +23 -15
  82. package/src/exec/bash-executor.ts +31 -5
  83. package/src/extensibility/extensions/loader.ts +16 -18
  84. package/src/extensibility/extensions/runner.ts +22 -17
  85. package/src/extensibility/extensions/types.ts +39 -5
  86. package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
  87. package/src/extensibility/skills.ts +0 -1
  88. package/src/internal-urls/docs-index.generated.ts +14 -13
  89. package/src/lsp/diagnostics-ledger.ts +51 -0
  90. package/src/lsp/index.ts +9 -22
  91. package/src/lsp/utils.ts +21 -0
  92. package/src/main.ts +92 -24
  93. package/src/modes/acp/acp-agent.ts +8 -4
  94. package/src/modes/acp/acp-event-mapper.ts +54 -4
  95. package/src/modes/components/assistant-message.ts +28 -1
  96. package/src/modes/components/custom-editor.ts +19 -7
  97. package/src/modes/components/hook-selector.ts +229 -44
  98. package/src/modes/components/oauth-selector.ts +12 -6
  99. package/src/modes/components/session-selector.ts +179 -24
  100. package/src/modes/components/tool-execution.ts +36 -7
  101. package/src/modes/controllers/command-controller.ts +2 -11
  102. package/src/modes/controllers/event-controller.ts +5 -2
  103. package/src/modes/controllers/extension-ui-controller.ts +6 -4
  104. package/src/modes/controllers/input-controller.ts +19 -16
  105. package/src/modes/controllers/selector-controller.ts +61 -21
  106. package/src/modes/interactive-mode.ts +127 -16
  107. package/src/modes/rpc/rpc-mode.ts +17 -6
  108. package/src/modes/theme/theme-schema.json +30 -0
  109. package/src/modes/theme/theme.ts +39 -2
  110. package/src/modes/types.ts +7 -3
  111. package/src/modes/utils/ui-helpers.ts +5 -2
  112. package/src/prompts/system/orchestrate-notice.md +5 -3
  113. package/src/prompts/system/workflow-notice.md +2 -2
  114. package/src/prompts/tools/ask.md +2 -1
  115. package/src/prompts/tools/eval.md +6 -6
  116. package/src/prompts/tools/find.md +1 -1
  117. package/src/prompts/tools/irc.md +6 -6
  118. package/src/prompts/tools/search.md +1 -1
  119. package/src/prompts/tools/task.md +1 -1
  120. package/src/registry/agent-registry.ts +1 -1
  121. package/src/sdk.ts +85 -31
  122. package/src/session/agent-session.ts +127 -57
  123. package/src/session/history-storage.ts +56 -12
  124. package/src/session/session-manager.ts +34 -0
  125. package/src/task/output-manager.ts +40 -48
  126. package/src/task/render.ts +3 -8
  127. package/src/tools/ask.ts +74 -32
  128. package/src/tools/browser/tab-worker.ts +8 -5
  129. package/src/tools/eval-backends.ts +38 -0
  130. package/src/tools/eval-render.ts +750 -0
  131. package/src/tools/eval.ts +27 -754
  132. package/src/tools/find.ts +5 -29
  133. package/src/tools/index.ts +8 -38
  134. package/src/tools/path-utils.ts +144 -1
  135. package/src/tools/read.ts +47 -0
  136. package/src/tools/renderers.ts +1 -1
  137. package/src/tools/search.ts +2 -27
  138. package/src/tools/sqlite-reader.ts +92 -9
  139. package/src/tools/write.ts +9 -1
  140. package/src/tui/output-block.ts +5 -4
  141. package/src/utils/clipboard.ts +38 -1
  142. package/src/utils/open.ts +37 -2
  143. package/src/web/kagi.ts +168 -49
  144. package/src/web/search/providers/anthropic.ts +1 -1
  145. package/src/web/search/providers/exa.ts +20 -86
  146. package/src/web/search/providers/kagi.ts +4 -0
@@ -0,0 +1,51 @@
1
+ import type { FileDiagnosticsResult } from "./index";
2
+ import { summarizeDiagnosticMessages } from "./utils";
3
+
4
+ const DIAGNOSTIC_LOCATION_PREFIX_RE = /^.*?:\d+:\d+\s+/;
5
+
6
+ export function diagnosticIdentity(message: string): string {
7
+ return message.replace(DIAGNOSTIC_LOCATION_PREFIX_RE, "");
8
+ }
9
+
10
+ export class DiagnosticsLedger {
11
+ readonly #seen = new Map<string, Set<string>>();
12
+
13
+ reduce(absPath: string, result: FileDiagnosticsResult): FileDiagnosticsResult {
14
+ const previous = this.#seen.get(absPath);
15
+ const currentIdentities = new Set<string>();
16
+ const fresh: string[] = [];
17
+
18
+ for (const message of result.messages) {
19
+ const identity = diagnosticIdentity(message);
20
+ currentIdentities.add(identity);
21
+ if (!previous?.has(identity)) {
22
+ fresh.push(message);
23
+ }
24
+ }
25
+
26
+ if (currentIdentities.size === 0) {
27
+ this.#seen.delete(absPath);
28
+ } else {
29
+ this.#seen.set(absPath, currentIdentities);
30
+ }
31
+
32
+ if (fresh.length === result.messages.length) {
33
+ return result;
34
+ }
35
+
36
+ return {
37
+ ...result,
38
+ messages: fresh,
39
+ ...summarizeDiagnosticMessages(fresh),
40
+ };
41
+ }
42
+ }
43
+
44
+ export interface DiagnosticsLedgerOwner {
45
+ diagnosticsLedger?: DiagnosticsLedger;
46
+ }
47
+
48
+ export function getDiagnosticsLedger(owner: DiagnosticsLedgerOwner): DiagnosticsLedger {
49
+ owner.diagnosticsLedger ??= new DiagnosticsLedger();
50
+ return owner.diagnosticsLedger;
51
+ }
package/src/lsp/index.ts CHANGED
@@ -79,6 +79,7 @@ import {
79
79
  resolveDiagnosticTargets,
80
80
  resolveSymbolColumn,
81
81
  sortDiagnostics,
82
+ summarizeDiagnosticMessages,
82
83
  symbolKindToIcon,
83
84
  uriToFile,
84
85
  } from "./utils";
@@ -816,12 +817,15 @@ export interface WritethroughOptions {
816
817
  onDeferredDiagnostics?: (diagnostics: FileDiagnosticsResult) => void;
817
818
  /** Signal to cancel a pending deferred diagnostics fetch. */
818
819
  deferredSignal?: AbortSignal;
820
+ /** Transform diagnostics before surfacing them after a successful fetch. */
821
+ transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
819
822
  }
820
823
 
821
824
  /** Internal resolved form of {@link WritethroughOptions} that the writethrough machinery operates on. */
822
825
  type ResolvedWritethroughOptions = {
823
826
  enableFormat: boolean;
824
827
  enableDiagnostics: boolean;
828
+ transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
825
829
  };
826
830
 
827
831
  /** Per-file deferred LSP diagnostics wiring for {@link WritethroughCallback}. */
@@ -881,6 +885,7 @@ function getOrCreateWritethroughBatch(id: string, options: ResolvedWritethroughO
881
885
  if (existing) {
882
886
  existing.options.enableFormat ||= options.enableFormat;
883
887
  existing.options.enableDiagnostics ||= options.enableDiagnostics;
888
+ existing.options.transformDiagnostics ??= options.transformDiagnostics;
884
889
  return existing;
885
890
  }
886
891
  const batch: LspWritethroughBatchState = {
@@ -904,27 +909,6 @@ export async function flushLspWritethroughBatch(
904
909
  return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
905
910
  }
906
911
 
907
- function summarizeDiagnosticMessages(messages: string[]): { summary: string; errored: boolean } {
908
- const counts = { error: 0, warning: 0, info: 0, hint: 0 };
909
- for (const message of messages) {
910
- const match = message.match(/\[(error|warning|info|hint)\]/i);
911
- if (!match) continue;
912
- const key = match[1].toLowerCase() as keyof typeof counts;
913
- counts[key] += 1;
914
- }
915
-
916
- const parts: string[] = [];
917
- if (counts.error > 0) parts.push(`${counts.error} error(s)`);
918
- if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`);
919
- if (counts.info > 0) parts.push(`${counts.info} info(s)`);
920
- if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`);
921
-
922
- return {
923
- summary: parts.length > 0 ? parts.join(", ") : "no issues",
924
- errored: counts.error > 0,
925
- };
926
- }
927
-
928
912
  function mergeDiagnostics(
929
913
  results: Array<FileDiagnosticsResult | undefined>,
930
914
  options: ResolvedWritethroughOptions,
@@ -1083,12 +1067,14 @@ async function runLspWritethrough(
1083
1067
 
1084
1068
  // 6. Get diagnostics from all servers (wait for fresh results)
1085
1069
  if (enableDiagnostics) {
1086
- diagnostics = await getDiagnosticsForFile(dst, cwd, servers, {
1070
+ const fetched = await getDiagnosticsForFile(dst, cwd, servers, {
1087
1071
  signal: operationSignal,
1088
1072
  minVersions,
1089
1073
  expectedDocumentVersions,
1090
1074
  allowUnversionedLspDiagnostics: false,
1091
1075
  });
1076
+ diagnostics =
1077
+ fetched && options.transformDiagnostics ? options.transformDiagnostics(dst, fetched) : fetched;
1092
1078
  }
1093
1079
  });
1094
1080
  } catch {
@@ -1155,6 +1141,7 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
1155
1141
  const resolvedOptions: ResolvedWritethroughOptions = {
1156
1142
  enableFormat: options?.enableFormat ?? false,
1157
1143
  enableDiagnostics: options?.enableDiagnostics ?? false,
1144
+ transformDiagnostics: options?.transformDiagnostics,
1158
1145
  };
1159
1146
  if (!resolvedOptions.enableFormat && !resolvedOptions.enableDiagnostics) {
1160
1147
  return writethroughNoop;
package/src/lsp/utils.ts CHANGED
@@ -221,6 +221,27 @@ export function formatDiagnosticsSummary(diagnostics: Diagnostic[]): string {
221
221
  return parts.length > 0 ? parts.join(", ") : "no issues";
222
222
  }
223
223
 
224
+ export function summarizeDiagnosticMessages(messages: string[]): { summary: string; errored: boolean } {
225
+ const counts = { error: 0, warning: 0, info: 0, hint: 0 };
226
+ for (const message of messages) {
227
+ const match = message.match(/\[(error|warning|info|hint)\]/i);
228
+ if (!match) continue;
229
+ const key = match[1].toLowerCase() as keyof typeof counts;
230
+ counts[key] += 1;
231
+ }
232
+
233
+ const parts: string[] = [];
234
+ if (counts.error > 0) parts.push(`${counts.error} error(s)`);
235
+ if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`);
236
+ if (counts.info > 0) parts.push(`${counts.info} info(s)`);
237
+ if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`);
238
+
239
+ return {
240
+ summary: parts.length > 0 ? parts.join(", ") : "no issues",
241
+ errored: counts.error > 0,
242
+ };
243
+ }
244
+
224
245
  // =============================================================================
225
246
  // Location Formatting
226
247
  // =============================================================================
package/src/main.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  VERSION,
22
22
  } from "@oh-my-pi/pi-utils";
23
23
  import chalk from "chalk";
24
+ import { reset as resetCapabilities } from "./capability";
24
25
  import type { Args } from "./cli/args";
25
26
  import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
26
27
  import { processFileArguments } from "./cli/file-processor";
@@ -91,15 +92,11 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
91
92
  }
92
93
  }
93
94
 
94
- const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
95
+ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
95
96
  "todo.enabled",
96
97
  "todo.reminders",
97
98
  "todo.reminders.max",
98
99
  "todo.eager",
99
- "async.enabled",
100
- "async.maxJobs",
101
- "bash.autoBackground.enabled",
102
- "bash.autoBackground.thresholdMs",
103
100
  "task.isolation.mode",
104
101
  "task.isolation.merge",
105
102
  "task.isolation.commits",
@@ -109,18 +106,34 @@ const RPC_DEFAULTED_SETTING_PATHS: SettingPath[] = [
109
106
  "task.maxRecursionDepth",
110
107
  "task.disabledAgents",
111
108
  "task.agentModelOverrides",
112
- // Memory subsystems are off-by-default for RPC hosts; embedders that want
109
+ // Memory subsystems are off-by-default for RPC/ACP hosts; embedders that want
113
110
  // memory should opt in explicitly through their own settings layer.
114
111
  "memory.backend",
115
112
  "memories.enabled",
116
113
  ];
117
114
 
118
- function applyRpcDefaultSettingOverrides(targetSettings: Settings = settings): void {
119
- for (const settingPath of RPC_DEFAULTED_SETTING_PATHS) {
115
+ const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
116
+ "async.enabled",
117
+ "async.maxJobs",
118
+ "bash.autoBackground.enabled",
119
+ "bash.autoBackground.thresholdMs",
120
+ ];
121
+
122
+ function applyDefaultSettingOverrides(settingPaths: SettingPath[], targetSettings: Settings): void {
123
+ for (const settingPath of settingPaths) {
120
124
  targetSettings.override(settingPath, getDefault(settingPath));
121
125
  }
122
126
  }
123
127
 
128
+ function applyRpcDefaultSettingOverrides(targetSettings: Settings = settings): void {
129
+ applyDefaultSettingOverrides(HOST_DEFAULTED_SETTING_PATHS, targetSettings);
130
+ applyDefaultSettingOverrides(RPC_BACKGROUND_DEFAULTED_SETTING_PATHS, targetSettings);
131
+ }
132
+
133
+ function applyAcpDefaultSettingOverrides(targetSettings: Settings = settings): void {
134
+ applyDefaultSettingOverrides(HOST_DEFAULTED_SETTING_PATHS, targetSettings);
135
+ }
136
+
124
137
  async function readPipedInput(): Promise<string | undefined> {
125
138
  if (process.stdin.isTTY !== false) return undefined;
126
139
  try {
@@ -312,15 +325,19 @@ async function runInteractiveMode(
312
325
  }
313
326
  }
314
327
 
315
- async function promptForkSession(session: SessionInfo): Promise<boolean> {
328
+ type ForkSessionPromptResult = "accepted" | "declined" | "unavailable";
329
+
330
+ type ForkSessionPrompt = (session: SessionInfo) => Promise<ForkSessionPromptResult>;
331
+
332
+ async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromptResult> {
316
333
  if (!process.stdin.isTTY) {
317
- return false;
334
+ return "unavailable";
318
335
  }
319
336
  const message = `Session found in different project: ${session.cwd}. Fork into current directory? [y/N] `;
320
337
  const rl = createInterface({ input: process.stdin, output: process.stdout });
321
338
  try {
322
339
  const answer = (await rl.question(message)).trim().toLowerCase();
323
- return answer === "y" || answer === "yes";
340
+ return answer === "y" || answer === "yes" ? "accepted" : "declined";
324
341
  } finally {
325
342
  rl.close();
326
343
  }
@@ -366,10 +383,12 @@ async function flushChangelogVersion(): Promise<void> {
366
383
  }
367
384
  }
368
385
 
369
- async function createSessionManager(
386
+ /** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */
387
+ export async function createSessionManager(
370
388
  parsed: Args,
371
389
  cwd: string,
372
390
  activeSettings: Settings = settings,
391
+ askToForkSession: ForkSessionPrompt = promptForkSession,
373
392
  ): Promise<SessionManager | undefined> {
374
393
  if (parsed.fork) {
375
394
  if (parsed.noSession) {
@@ -402,9 +421,17 @@ async function createSessionManager(
402
421
  const normalizedCwd = normalizePathForComparison(cwd);
403
422
  const normalizedMatchCwd = normalizePathForComparison(match.session.cwd || cwd);
404
423
  if (normalizedCwd !== normalizedMatchCwd) {
405
- const shouldFork = await promptForkSession(match.session);
406
- if (!shouldFork) {
407
- throw new Error(`Session "${sessionArg}" is in another project (${match.session.cwd}).`);
424
+ const forkPromptResult = await askToForkSession(match.session);
425
+ if (forkPromptResult === "unavailable") {
426
+ throw new Error(
427
+ `Session "${sessionArg}" is in another project (${match.session.cwd}); run interactively to fork it into the current project.`,
428
+ );
429
+ }
430
+ if (forkPromptResult === "declined") {
431
+ // User declined the cross-project fork prompt. Caller distinguishes
432
+ // this cancellation from the "default new session" undefined return
433
+ // by checking `typeof parsed.resume === "string"`.
434
+ return undefined;
408
435
  }
409
436
  return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir);
410
437
  }
@@ -776,15 +803,17 @@ export async function runRootCommand(
776
803
  }
777
804
  }
778
805
 
779
- const cwd = getProjectDir();
806
+ let cwd = getProjectDir();
780
807
  const settingsInstance = deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd }));
781
808
  if (parsedArgs.approvalMode) {
782
809
  // Runtime override (not persisted): every settings.get("tools.approvalMode") downstream
783
810
  // sees this value. The wrapper still honours --auto-approve / --yolo on top of it.
784
811
  settingsInstance.override("tools.approvalMode", parsedArgs.approvalMode);
785
812
  }
786
- if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
813
+ if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui") {
787
814
  applyRpcDefaultSettingOverrides(settingsInstance);
815
+ } else if (parsedArgs.mode === "acp") {
816
+ applyAcpDefaultSettingOverrides(settingsInstance);
788
817
  }
789
818
  if (parsedArgs.noPty || parsedArgs.mode === "rpc-ui") {
790
819
  Bun.env.PI_NO_PTY = "1";
@@ -812,6 +841,11 @@ export async function runRootCommand(
812
841
  });
813
842
  }
814
843
 
844
+ // Apply --hide-thinking CLI flag (ephemeral, not persisted)
845
+ if (parsedArgs.hideThinking) {
846
+ settingsInstance.override("hideThinkingBlock", true);
847
+ }
848
+
815
849
  await logger.time(
816
850
  "initTheme:final",
817
851
  initTheme,
@@ -846,19 +880,53 @@ export async function runRootCommand(
846
880
  settingsInstance,
847
881
  );
848
882
 
883
+ // User declined the cross-project fork prompt — exit cleanly with a friendly
884
+ // message rather than letting the decline bubble up as an uncaught exception
885
+ // (see issue #1668).
886
+ if (typeof parsedArgs.resume === "string" && !sessionManager) {
887
+ process.stdout.write(`${chalk.dim("Resume cancelled: session is in another project.")}\n`);
888
+ return;
889
+ }
890
+
849
891
  // Handle --resume (no value): show session picker
850
892
  if (parsedArgs.resume === true && !parsedArgs.fork) {
851
- const sessions = await logger.time("SessionManager.list", SessionManager.list, cwd, parsedArgs.sessionDir);
852
- if (sessions.length === 0) {
853
- process.stdout.write(`${chalk.dim("No sessions found")}\n`);
854
- return;
893
+ const folderSessions = await logger.time("SessionManager.list", SessionManager.list, cwd, parsedArgs.sessionDir);
894
+ let preloadedAllSessions: SessionInfo[] | undefined;
895
+ let startInAllScope = false;
896
+ if (folderSessions.length === 0) {
897
+ // Nothing in the current folder — fall back to a global scan so the
898
+ // picker can still open in all-projects scope instead of dead-ending.
899
+ preloadedAllSessions = await logger.time("SessionManager.listAll", SessionManager.listAll);
900
+ if (preloadedAllSessions.length === 0) {
901
+ process.stdout.write(`${chalk.dim("No sessions found")}\n`);
902
+ return;
903
+ }
904
+ startInAllScope = true;
855
905
  }
856
- const selectedPath = await logger.time("selectSession", selectSession, sessions);
857
- if (!selectedPath) {
906
+ const selected = await logger.time("selectSession", selectSession, folderSessions, {
907
+ allSessions: preloadedAllSessions,
908
+ startInAllScope,
909
+ });
910
+ if (!selected) {
858
911
  process.stdout.write(`${chalk.dim("No session selected")}\n`);
859
912
  return;
860
913
  }
861
- sessionManager = await SessionManager.open(selectedPath);
914
+ // Resuming a session from another project: switch the process into that
915
+ // project's directory and refresh cwd-derived caches before the session is
916
+ // built, so settings discovery, plugins, and capabilities all scope to it.
917
+ if (selected.cwd && normalizePathForComparison(selected.cwd) !== normalizePathForComparison(getProjectDir())) {
918
+ // Let the original (launch-cwd) plugin-root preload settle first so its
919
+ // late resolution can't clobber the re-warm we trigger below.
920
+ await pluginPreloadPromise.catch(() => {});
921
+ setProjectDir(selected.cwd);
922
+ clearPluginRootsAndCaches();
923
+ resetCapabilities();
924
+ cwd = getProjectDir();
925
+ // Re-scope project settings (.claude/settings.yml etc.) to the resumed
926
+ // project in place so the session is built with its configuration.
927
+ await settingsInstance.reloadForCwd(cwd);
928
+ }
929
+ sessionManager = await SessionManager.open(selected.path);
862
930
  }
863
931
 
864
932
  await pluginPreloadPromise;
@@ -47,7 +47,11 @@ import { logger, VERSION } from "@oh-my-pi/pi-utils";
47
47
  import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
48
48
  import { Settings } from "../../config/settings";
49
49
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
50
- import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
50
+ import {
51
+ type ExtensionUIContext,
52
+ type ExtensionUIDialogOptions,
53
+ getExtensionUISelectOptionLabel,
54
+ } from "../../extensibility/extensions";
51
55
  import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
52
56
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
53
57
  import { buildSkillPromptMessage, getSkillSlashCommandName } from "../../extensibility/skills";
@@ -302,7 +306,7 @@ export function createAcpExtensionUiContext(
302
306
  getSessionId(),
303
307
  "select",
304
308
  title,
305
- { type: "string", enum: options },
309
+ { type: "string", enum: options.map(getExtensionUISelectOptionLabel) },
306
310
  dialogOptions,
307
311
  );
308
312
  return typeof value === "string" ? value : undefined;
@@ -1981,7 +1985,7 @@ export class AcpAgent implements Agent {
1981
1985
  }
1982
1986
  if (servers.length === 0) {
1983
1987
  record.mcpManager = undefined;
1984
- await record.session.refreshMCPTools([]);
1988
+ await record.session.refreshMCPTools([], { activateAll: true });
1985
1989
  return;
1986
1990
  }
1987
1991
 
@@ -2008,7 +2012,7 @@ export class AcpAgent implements Agent {
2008
2012
  }
2009
2013
 
2010
2014
  record.mcpManager = manager;
2011
- await record.session.refreshMCPTools(result.tools);
2015
+ await record.session.refreshMCPTools(result.tools, { activateAll: true });
2012
2016
  }
2013
2017
 
2014
2018
  #toMcpConfig(server: McpServer): MCPServerConfig {
@@ -69,6 +69,16 @@ interface CommandContainer {
69
69
  command?: unknown;
70
70
  }
71
71
 
72
+ interface EvalCellContainer {
73
+ cells?: unknown;
74
+ }
75
+
76
+ interface EvalCellLike {
77
+ language?: unknown;
78
+ title?: unknown;
79
+ code?: unknown;
80
+ }
81
+
72
82
  interface PatternContainer {
73
83
  pattern?: unknown;
74
84
  }
@@ -435,11 +445,43 @@ function getToolExecutionEndArgs(
435
445
  }
436
446
 
437
447
  function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] {
438
- if (!isCommandToolName(toolName)) {
439
- return [];
448
+ const text = buildToolStartText(toolName, args);
449
+ return text ? [textToolCallContent(text)] : [];
450
+ }
451
+
452
+ function buildToolStartText(toolName: string, args: unknown): string | undefined {
453
+ if (isCommandToolName(toolName)) {
454
+ const command = extractStringProperty<CommandContainer>(args, "command");
455
+ return command ? limitText(`$ ${command}`) : undefined;
456
+ }
457
+ if (toolName === "eval") {
458
+ return buildEvalStartText(args);
459
+ }
460
+ return undefined;
461
+ }
462
+
463
+ function buildEvalStartText(args: unknown): string | undefined {
464
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
465
+ return undefined;
466
+ }
467
+ const cells = (args as EvalCellContainer).cells;
468
+ if (!Array.isArray(cells) || cells.length === 0) {
469
+ return undefined;
440
470
  }
441
- const command = extractStringProperty<CommandContainer>(args, "command");
442
- return command ? [textToolCallContent(`$ ${command}`)] : [];
471
+ const lines: string[] = [];
472
+ for (const cell of cells) {
473
+ if (typeof cell !== "object" || cell === null || Array.isArray(cell)) {
474
+ continue;
475
+ }
476
+ const language = extractStringProperty<EvalCellLike>(cell, "language") ?? "?";
477
+ const title = extractStringProperty<EvalCellLike>(cell, "title");
478
+ const code = extractStringProperty<EvalCellLike>(cell, "code");
479
+ if (!code) {
480
+ continue;
481
+ }
482
+ lines.push(title ? `[${language}] ${title}` : `[${language}]`, code);
483
+ }
484
+ return lines.length > 0 ? limitText(lines.join("\n")) : undefined;
443
485
  }
444
486
 
445
487
  function mergeToolUpdateContent(startContent: ToolCallContent[], resultContent: ToolCallContent[]): ToolCallContent[] {
@@ -465,6 +507,14 @@ function isCommandToolName(toolName: string): boolean {
465
507
  }
466
508
 
467
509
  function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string {
510
+ if (isCommandToolName(toolName)) {
511
+ const commandText = buildToolStartText(toolName, args);
512
+ if (commandText) return commandText;
513
+ }
514
+ if (toolName === "eval") {
515
+ const evalText = buildEvalStartText(args);
516
+ if (evalText) return evalText;
517
+ }
468
518
  const trimmedIntent = intent?.trim();
469
519
  if (trimmedIntent) {
470
520
  return trimmedIntent;
@@ -2,6 +2,7 @@ import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
2
2
  import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
3
3
  import { formatNumber } from "@oh-my-pi/pi-utils";
4
4
  import { settings } from "../../config/settings";
5
+ import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
5
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
6
7
  import { isSilentAbort } from "../../session/messages";
7
8
  import { resolveImageOptions } from "../../tools/render-utils";
@@ -21,6 +22,7 @@ export class AssistantMessageComponent extends Container {
21
22
  message?: AssistantMessage,
22
23
  private hideThinkingBlock = false,
23
24
  private readonly onImageUpdate?: () => void,
25
+ private readonly thinkingRenderers: readonly AssistantThinkingRenderer[] = [],
24
26
  ) {
25
27
  super();
26
28
 
@@ -131,6 +133,27 @@ export class AssistantMessageComponent extends Container {
131
133
  }
132
134
  }
133
135
 
136
+ #appendThinkingExtensions(contentIndex: number, thinkingIndex: number, text: string): void {
137
+ for (const renderer of this.thinkingRenderers) {
138
+ try {
139
+ const component = renderer(
140
+ {
141
+ contentIndex,
142
+ thinkingIndex,
143
+ text,
144
+ requestRender: () => this.onImageUpdate?.(),
145
+ },
146
+ theme,
147
+ );
148
+ if (component) {
149
+ this.#contentContainer.addChild(component);
150
+ }
151
+ } catch {
152
+ // Ignore extension renderer failures and keep the original thinking block visible.
153
+ }
154
+ }
155
+ }
156
+
134
157
  updateContent(message: AssistantMessage): void {
135
158
  this.#lastMessage = message;
136
159
 
@@ -146,6 +169,7 @@ export class AssistantMessageComponent extends Container {
146
169
  }
147
170
 
148
171
  // Render content in order
172
+ let thinkingIndex = 0;
149
173
  for (let i = 0; i < message.content.length; i++) {
150
174
  const content = message.content[i];
151
175
  if (content.type === "text" && content.text.trim()) {
@@ -166,13 +190,16 @@ export class AssistantMessageComponent extends Container {
166
190
  this.#contentContainer.addChild(new Spacer(1));
167
191
  }
168
192
  } else {
193
+ const thinkingText = content.thinking.trim();
169
194
  // Thinking traces in thinkingText color, italic
170
195
  this.#contentContainer.addChild(
171
- new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
196
+ new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
172
197
  color: (text: string) => theme.fg("thinkingText", text),
173
198
  italic: true,
174
199
  }),
175
200
  );
201
+ this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
202
+ thinkingIndex += 1;
176
203
  if (hasVisibleContentAfter) {
177
204
  this.#contentContainer.addChild(new Spacer(1));
178
205
  }
@@ -19,6 +19,7 @@ type ConfigurableEditorAction = Extract<
19
19
  | "app.history.search"
20
20
  | "app.message.dequeue"
21
21
  | "app.clipboard.pasteImage"
22
+ | "app.clipboard.pasteTextRaw"
22
23
  | "app.clipboard.copyPrompt"
23
24
  >;
24
25
 
@@ -38,6 +39,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
38
39
  "app.history.search": ["ctrl+r"],
39
40
  "app.message.dequeue": ["alt+up"],
40
41
  "app.clipboard.pasteImage": ["ctrl+v"],
42
+ "app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
41
43
  "app.clipboard.copyPrompt": ["alt+shift+c"],
42
44
  };
43
45
 
@@ -49,7 +51,6 @@ export class CustomEditor extends Editor {
49
51
  * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. */
50
52
  decorateText = (text: string): string => highlightMagicKeywords(text);
51
53
  onEscape?: () => void;
52
- shouldBypassAutocompleteOnEscape?: () => boolean;
53
54
  onClear?: () => void;
54
55
  onExit?: () => void;
55
56
  onCycleThinkingLevel?: () => void;
@@ -66,6 +67,8 @@ export class CustomEditor extends Editor {
66
67
  onCopyPrompt?: () => void;
67
68
  /** Called when the configured image-paste shortcut is pressed. */
68
69
  onPasteImage?: () => Promise<boolean>;
70
+ /** Called when the configured raw text-paste shortcut is pressed. */
71
+ onPasteTextRaw?: () => void;
69
72
  /** Called when the configured dequeue shortcut is pressed. */
70
73
  onDequeue?: () => void;
71
74
  /** Called when Caps Lock is pressed. */
@@ -125,6 +128,12 @@ export class CustomEditor extends Editor {
125
128
  return;
126
129
  }
127
130
 
131
+ // Intercept configured raw text paste (fires and handles result)
132
+ if (this.#matchesAction(data, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
133
+ this.onPasteTextRaw();
134
+ return;
135
+ }
136
+
128
137
  // Intercept configured external editor shortcut
129
138
  if (this.#matchesAction(data, "app.editor.external") && this.onExternalEditor) {
130
139
  this.onExternalEditor();
@@ -186,12 +195,15 @@ export class CustomEditor extends Editor {
186
195
  }
187
196
 
188
197
  // Intercept configured interrupt shortcut.
189
- // Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
190
- if (this.#matchesAction(data, "app.interrupt") && this.onEscape) {
191
- if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
192
- this.onEscape();
193
- return;
194
- }
198
+ // When the autocomplete popup is visible, ESC's first job is to dismiss
199
+ // the popup — let super.handleInput() route it to #cancelAutocomplete().
200
+ // The user can press ESC again afterward to fire the global interrupt
201
+ // handler. This matches the standard TUI/IDE pattern and prevents a
202
+ // single ESC from both closing an @ completion and aborting an active
203
+ // agent run (#1655).
204
+ if (this.#matchesAction(data, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
205
+ this.onEscape();
206
+ return;
195
207
  }
196
208
 
197
209
  // Intercept configured clear shortcut