@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.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 (180) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
  3. package/dist/cli.js +9986 -9776
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/settings-schema.d.ts +63 -8
  13. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  14. package/dist/types/cursor.d.ts +193 -8
  15. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  16. package/dist/types/edit/hashline/diff.d.ts +9 -1
  17. package/dist/types/edit/index.d.ts +8 -1
  18. package/dist/types/edit/streaming.d.ts +8 -1
  19. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  20. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  21. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  22. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  23. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  24. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  25. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  26. package/dist/types/lsp/client.d.ts +2 -0
  27. package/dist/types/main.d.ts +2 -0
  28. package/dist/types/mcp/manager.d.ts +33 -4
  29. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  30. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  31. package/dist/types/modes/components/session-selector.d.ts +6 -0
  32. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  33. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  34. package/dist/types/modes/components/usage-row.d.ts +2 -0
  35. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  36. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  37. package/dist/types/modes/interactive-mode.d.ts +4 -2
  38. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  39. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  40. package/dist/types/modes/theme/theme.d.ts +12 -1
  41. package/dist/types/modes/types.d.ts +2 -1
  42. package/dist/types/sdk.d.ts +1 -0
  43. package/dist/types/session/agent-session-events.d.ts +2 -0
  44. package/dist/types/session/agent-session-types.d.ts +36 -1
  45. package/dist/types/session/agent-session.d.ts +4 -2
  46. package/dist/types/session/claude-session-store.d.ts +13 -0
  47. package/dist/types/session/codex-session-store.d.ts +14 -0
  48. package/dist/types/session/credential-pin.d.ts +58 -0
  49. package/dist/types/session/foreign-session-import.d.ts +15 -0
  50. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  51. package/dist/types/session/foreign-session-store.d.ts +23 -0
  52. package/dist/types/session/session-advisors.d.ts +47 -2
  53. package/dist/types/session/session-entries.d.ts +19 -1
  54. package/dist/types/session/session-handoff.d.ts +4 -1
  55. package/dist/types/session/session-loader.d.ts +2 -0
  56. package/dist/types/session/session-maintenance.d.ts +1 -1
  57. package/dist/types/session/session-manager.d.ts +21 -0
  58. package/dist/types/session/session-tools.d.ts +25 -1
  59. package/dist/types/session/turn-recovery.d.ts +10 -4
  60. package/dist/types/system-prompt.d.ts +2 -1
  61. package/dist/types/task/executor.d.ts +11 -2
  62. package/dist/types/task/render.d.ts +7 -0
  63. package/dist/types/thinking.d.ts +18 -6
  64. package/dist/types/tools/acp-bridge.d.ts +28 -5
  65. package/dist/types/tools/grep.d.ts +17 -1
  66. package/dist/types/tools/hub/launch.d.ts +5 -1
  67. package/dist/types/tools/index.d.ts +4 -1
  68. package/dist/types/tools/path-utils.d.ts +19 -0
  69. package/dist/types/tools/read.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +2 -1
  71. package/dist/types/utils/changelog.d.ts +19 -0
  72. package/dist/types/utils/git.d.ts +10 -0
  73. package/dist/types/utils/jj.d.ts +11 -4
  74. package/dist/types/web/search/types.d.ts +2 -2
  75. package/package.json +12 -12
  76. package/src/advisor/runtime.ts +138 -12
  77. package/src/advisor/transcript-recorder.ts +56 -0
  78. package/src/auto-thinking/classifier.ts +48 -10
  79. package/src/cli/args.ts +6 -0
  80. package/src/cli/flag-tables.ts +2 -0
  81. package/src/cli/gallery-fixtures/fs.ts +25 -0
  82. package/src/cli/session-picker.ts +34 -17
  83. package/src/cli/ttsr-cli.ts +19 -1
  84. package/src/cli.ts +9 -0
  85. package/src/collab/guest.ts +37 -4
  86. package/src/collab/host.ts +1 -0
  87. package/src/commands/launch.ts +6 -0
  88. package/src/config/model-registry.ts +103 -25
  89. package/src/config/settings-schema.ts +65 -6
  90. package/src/config/settings.ts +25 -0
  91. package/src/cursor-bridge-tools.ts +81 -0
  92. package/src/cursor.ts +463 -12
  93. package/src/edit/edit-clipboard.ts +23 -0
  94. package/src/edit/hashline/diff.ts +49 -10
  95. package/src/edit/hashline/execute.ts +38 -6
  96. package/src/edit/hashline/filesystem.ts +27 -3
  97. package/src/edit/index.ts +12 -2
  98. package/src/edit/renderer.ts +4 -4
  99. package/src/edit/streaming.ts +17 -2
  100. package/src/export/html/template.js +1 -1
  101. package/src/extensibility/extensions/runner.ts +70 -0
  102. package/src/extensibility/extensions/types.ts +30 -0
  103. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  104. package/src/internal-urls/local-protocol.ts +3 -2
  105. package/src/internal-urls/mcp-protocol.ts +4 -1
  106. package/src/launch/terminal-output-worker-client.ts +53 -0
  107. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  108. package/src/launch/terminal-output-worker.ts +23 -0
  109. package/src/lsp/client.ts +18 -3
  110. package/src/lsp/index.ts +4 -0
  111. package/src/lsp/render.ts +1 -1
  112. package/src/main.ts +127 -56
  113. package/src/mcp/manager.ts +120 -18
  114. package/src/modes/acp/acp-agent.ts +15 -9
  115. package/src/modes/components/chat-transcript-builder.ts +30 -11
  116. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  117. package/src/modes/components/read-tool-group.ts +159 -17
  118. package/src/modes/components/session-selector.ts +19 -3
  119. package/src/modes/components/status-line/component.ts +381 -54
  120. package/src/modes/components/tool-execution.ts +78 -31
  121. package/src/modes/components/usage-row.ts +10 -5
  122. package/src/modes/controllers/event-controller.ts +26 -4
  123. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  124. package/src/modes/controllers/selector-controller.ts +94 -36
  125. package/src/modes/controllers/tan-command-controller.ts +14 -0
  126. package/src/modes/interactive-mode.ts +31 -17
  127. package/src/modes/rpc/rpc-client.ts +18 -0
  128. package/src/modes/rpc/rpc-mode.ts +16 -1
  129. package/src/modes/rpc/rpc-types.ts +11 -0
  130. package/src/modes/theme/theme.ts +151 -6
  131. package/src/modes/types.ts +2 -1
  132. package/src/modes/utils/ui-helpers.ts +38 -21
  133. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  134. package/src/prompts/system/plan-mode-active.md +2 -2
  135. package/src/prompts/system/system-prompt.md +3 -0
  136. package/src/prompts/system/xdev-mount-notice.md +1 -1
  137. package/src/prompts/tools/bash.md +15 -17
  138. package/src/prompts/tools/checkpoint.md +1 -1
  139. package/src/prompts/tools/glob.md +7 -6
  140. package/src/prompts/tools/grep.md +7 -6
  141. package/src/sdk.ts +178 -15
  142. package/src/session/agent-session-events.ts +1 -0
  143. package/src/session/agent-session-types.ts +40 -1
  144. package/src/session/agent-session.ts +302 -175
  145. package/src/session/claude-session-store.ts +426 -0
  146. package/src/session/codex-session-store.ts +673 -0
  147. package/src/session/credential-pin.ts +93 -0
  148. package/src/session/foreign-session-import.ts +52 -0
  149. package/src/session/foreign-session-jsonl.ts +29 -0
  150. package/src/session/foreign-session-store.ts +26 -0
  151. package/src/session/model-controls.ts +11 -3
  152. package/src/session/session-advisors.ts +140 -16
  153. package/src/session/session-entries.ts +21 -1
  154. package/src/session/session-handoff.ts +20 -3
  155. package/src/session/session-loader.ts +25 -9
  156. package/src/session/session-maintenance.ts +59 -11
  157. package/src/session/session-manager.ts +61 -0
  158. package/src/session/session-tools.ts +107 -5
  159. package/src/session/settings-stream-fn.ts +1 -0
  160. package/src/session/turn-recovery.ts +97 -51
  161. package/src/slash-commands/builtin-registry.ts +131 -3
  162. package/src/system-prompt.ts +3 -2
  163. package/src/task/executor.ts +17 -4
  164. package/src/task/render.ts +20 -4
  165. package/src/thinking.ts +29 -10
  166. package/src/tools/acp-bridge.ts +52 -8
  167. package/src/tools/browser.ts +5 -0
  168. package/src/tools/checkpoint.ts +0 -13
  169. package/src/tools/grep.ts +64 -8
  170. package/src/tools/hub/launch.ts +10 -15
  171. package/src/tools/index.ts +28 -4
  172. package/src/tools/path-utils.ts +88 -0
  173. package/src/tools/read.ts +3 -0
  174. package/src/tools/write.ts +9 -3
  175. package/src/tools/xdev.ts +39 -14
  176. package/src/utils/changelog.ts +144 -4
  177. package/src/utils/git.ts +61 -14
  178. package/src/utils/jj.ts +19 -13
  179. package/src/web/search/providers/codex.ts +34 -0
  180. package/src/web/search/types.ts +1 -1
@@ -97,7 +97,10 @@ function formatAvailableResources(mcpManager: MCPManager): string {
97
97
  .getConnectedServers()
98
98
  .flatMap(name => {
99
99
  const serverResources = mcpManager.getServerResources(name);
100
- return (serverResources?.resources ?? []).map(r => ` ${r.uri} (${name})`);
100
+ if (!serverResources) return [];
101
+ const concrete = serverResources.resources.map(r => ` ${r.uri} (${name})`);
102
+ const templates = serverResources.templates.map(t => ` ${t.uriTemplate} (${name}, template)`);
103
+ return [...concrete, ...templates];
101
104
  })
102
105
  .join("\n");
103
106
  return available || " (none)";
@@ -0,0 +1,53 @@
1
+ import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
2
+ import type { TerminalOutputOptions } from "./terminal-output";
3
+ import {
4
+ TERMINAL_OUTPUT_WORKER_ARG,
5
+ type TerminalOutputWorkerRequest,
6
+ type TerminalOutputWorkerResult,
7
+ } from "./terminal-output-worker-protocol";
8
+
9
+ /** Replay legacy broker PTY bytes without evaluating xterm in the client process. */
10
+ export async function renderTerminalOutputIsolated(
11
+ output: string,
12
+ options: TerminalOutputOptions,
13
+ ): Promise<string[] | undefined> {
14
+ const hostEntry = workerHostEntry();
15
+ const worker = hostEntry
16
+ ? new Worker(hostEntry, { type: "module", argv: [TERMINAL_OUTPUT_WORKER_ARG] })
17
+ : new Worker(new URL("./terminal-output-worker.ts", import.meta.url).href, { type: "module" });
18
+ const pending = Promise.withResolvers<string[] | undefined>();
19
+ const onMessage = (event: MessageEvent<TerminalOutputWorkerResult>): void => {
20
+ if (event.data.ok) pending.resolve(event.data.rows);
21
+ else pending.reject(new Error(event.data.error));
22
+ };
23
+ const onError = (event: ErrorEvent): void => {
24
+ pending.reject(event.error instanceof Error ? event.error : new Error(event.message));
25
+ };
26
+ const onClose = (): void => {
27
+ pending.reject(new Error("Terminal output worker exited before responding"));
28
+ };
29
+ worker.addEventListener("message", onMessage);
30
+ worker.addEventListener("error", onError);
31
+ worker.addEventListener("close", onClose);
32
+ try {
33
+ const request: TerminalOutputWorkerRequest = { output, options };
34
+ worker.postMessage(request);
35
+ return await pending.promise;
36
+ } finally {
37
+ worker.removeEventListener("message", onMessage);
38
+ worker.removeEventListener("error", onError);
39
+ worker.removeEventListener("close", onClose);
40
+ worker.terminate();
41
+ }
42
+ }
43
+
44
+ /** Distribution smoke for source, npm-bundle, and compiled worker routing. */
45
+ export async function smokeTestTerminalOutputWorker(): Promise<void> {
46
+ const rows = await renderTerminalOutputIsolated("old\r\x1b[2K\x1b[1;32mready\x1b[0m", {
47
+ head: false,
48
+ maxRows: 10,
49
+ });
50
+ if (rows?.length !== 1 || rows[0] !== "\x1b[0m\x1b[1;38;5;2mready") {
51
+ throw new Error("terminal output worker smoke mismatch");
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ import type { TerminalOutputOptions } from "./terminal-output";
2
+
3
+ /** Hidden CLI selector for legacy PTY replay outside the client process. */
4
+ export const TERMINAL_OUTPUT_WORKER_ARG = "__omp_worker_terminal_output";
5
+
6
+ export interface TerminalOutputWorkerRequest {
7
+ output: string;
8
+ options: TerminalOutputOptions;
9
+ }
10
+
11
+ export type TerminalOutputWorkerResult = { ok: true; rows: string[] | undefined } | { ok: false; error: string };
@@ -0,0 +1,23 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
3
+ import { renderTerminalOutput } from "./terminal-output";
4
+ import type { TerminalOutputWorkerRequest, TerminalOutputWorkerResult } from "./terminal-output-worker-protocol";
5
+
6
+ if (!parentPort) throw new Error("terminal-output-worker: missing parentPort");
7
+
8
+ const port = parentPort;
9
+ const inbox = consumeWorkerInbox();
10
+ const handle = async (message: unknown): Promise<void> => {
11
+ const request = message as TerminalOutputWorkerRequest;
12
+ let result: TerminalOutputWorkerResult;
13
+ try {
14
+ result = { ok: true, rows: await renderTerminalOutput(request.output, request.options) };
15
+ } catch (error) {
16
+ result = { ok: false, error: error instanceof Error ? error.message : String(error) };
17
+ }
18
+ port.postMessage(result);
19
+ port.close();
20
+ };
21
+
22
+ if (inbox) inbox.bind(message => void handle(message));
23
+ else port.on("message", message => void handle(message));
package/src/lsp/client.ts CHANGED
@@ -27,6 +27,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
27
27
  /** Negative cache of recent init failures so a broken server fails fast instead of re-spawning per call. */
28
28
  const INIT_FAILURE_BACKOFF_MS = 3 * 60 * 1000;
29
29
  const initFailures = new Map<string, { at: number; message: string }>();
30
+ const READER_EXIT_GRACE_MS = 100;
30
31
 
31
32
  // Idle timeout configuration (disabled by default)
32
33
  let idleTimeoutMs: number | null = null;
@@ -303,6 +304,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
303
304
 
304
305
  const framer = new MessageFramer(Buffer.from(client.messageBuffer));
305
306
 
307
+ let readerFailed = false;
306
308
  try {
307
309
  while (true) {
308
310
  const { done, value } = await reader.read();
@@ -391,6 +393,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
391
393
  }
392
394
  }
393
395
  } catch (err) {
396
+ readerFailed = true;
394
397
  // Connection closed or error - reject all pending requests
395
398
  for (const pending of Array.from(client.pendingRequests.values())) {
396
399
  pending.reject(new Error(`LSP connection closed: ${err}`));
@@ -401,6 +404,9 @@ async function startMessageReader(client: LspClient): Promise<void> {
401
404
  client.messageBuffer = framer.remainder();
402
405
  reader.releaseLock();
403
406
  client.isReading = false;
407
+ if (!readerFailed && client.proc.exitCode === null) {
408
+ await waitForExit(client, READER_EXIT_GRACE_MS);
409
+ }
404
410
  // Reader exited while the server process is still alive (unrecoverable
405
411
  // read error or bad stream state): nothing will route responses anymore,
406
412
  // so tear the client down — the next call respawns instead of timing out.
@@ -676,6 +682,15 @@ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
676
682
  const SHUTDOWN_TIMEOUT_MS = 5_000;
677
683
  const EXIT_TIMEOUT_MS = 1_000;
678
684
 
685
+ function clientKey(config: ServerConfig, cwd: string): string {
686
+ return `${config.command}:${cwd}`;
687
+ }
688
+
689
+ /** Allow an explicit user reload to retry a matching initialization failure immediately. */
690
+ export function clearInitializationFailure(config: ServerConfig, cwd: string): void {
691
+ initFailures.delete(clientKey(config, cwd));
692
+ }
693
+
679
694
  /**
680
695
  * Get or create an LSP client for the given server configuration and working directory.
681
696
  * @param config - Server configuration
@@ -692,7 +707,7 @@ export async function getOrCreateClient(
692
707
  initTimeoutMs?: number,
693
708
  signal?: AbortSignal,
694
709
  ): Promise<LspClient> {
695
- const key = `${config.command}:${cwd}`;
710
+ const key = clientKey(config, cwd);
696
711
 
697
712
  // Check if client already exists
698
713
  const existingClient = clients.get(key);
@@ -865,13 +880,13 @@ export async function getActiveOrPendingClient(
865
880
  signal?: AbortSignal,
866
881
  ): Promise<LspClient | undefined> {
867
882
  throwIfAborted(signal);
868
- const client = clients.get(`${config.command}:${cwd}`);
883
+ const client = clients.get(clientKey(config, cwd));
869
884
  if (client) {
870
885
  client.lastActivity = Date.now();
871
886
  return client;
872
887
  }
873
888
 
874
- const pending = clientLocks.get(`${config.command}:${cwd}`);
889
+ const pending = clientLocks.get(clientKey(config, cwd));
875
890
  if (!pending) return undefined;
876
891
  try {
877
892
  return await untilAborted(signal, pending);
package/src/lsp/index.ts CHANGED
@@ -17,6 +17,7 @@ import { formatPathRelativeToCwd, resolveToCwd } from "../tools/path-utils";
17
17
  import { ToolAbortError, ToolError, throwIfAborted } from "../tools/tool-errors";
18
18
  import { clampTimeout } from "../tools/tool-timeouts";
19
19
  import {
20
+ clearInitializationFailure,
20
21
  ensureFileOpen,
21
22
  FileChangeType,
22
23
  getActiveClients,
@@ -2345,6 +2346,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2345
2346
  const outputs: string[] = [];
2346
2347
  for (const [workspaceServerName, workspaceServerConfig] of servers) {
2347
2348
  throwIfAborted(signal);
2349
+ clearInitializationFailure(workspaceServerConfig, this.session.cwd);
2348
2350
  try {
2349
2351
  const workspaceClient = await getOrCreateClient(
2350
2352
  workspaceServerConfig,
@@ -2377,6 +2379,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2377
2379
 
2378
2380
  const [serverName, serverConfig] = serverInfo;
2379
2381
 
2382
+ if (action === "reload") clearInitializationFailure(serverConfig, this.session.cwd);
2383
+
2380
2384
  try {
2381
2385
  const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
2382
2386
  const targetFile = resolvedFile;
package/src/lsp/render.ts CHANGED
@@ -582,7 +582,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
582
582
  for (let i = 0; i < lines.length; i++) {
583
583
  const isLast = i === lines.length - 1;
584
584
  const branch = isLast ? theme.tree.last : theme.tree.branch;
585
- output += `\n ${theme.fg("dim", branch)} ${lines[i]}`;
585
+ output += `\n ${theme.fg("dim", branch)} ${truncateToWidth(replaceTabs(lines[i]), TRUNCATE_LENGTHS.CONTENT)}`;
586
586
  }
587
587
  return output.split("\n");
588
588
  }
package/src/main.ts CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  type ScopedModel,
42
42
  } from "./config/model-resolver";
43
43
  import { ModelsConfigFile } from "./config/models-config";
44
- import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
44
+ import { getDefault, type SettingPath, Settings, type SettingValue, settings } from "./config/settings";
45
45
  import { initializeWithSettings } from "./discovery";
46
46
  import {
47
47
  clearPluginRootsAndCaches,
@@ -74,6 +74,13 @@ import {
74
74
  import type { AgentSession } from "./session/agent-session";
75
75
  import type { AuthStorage } from "./session/auth-storage";
76
76
  import { describePendingToolCalls } from "./session/exit-diagnostics";
77
+ import {
78
+ createForeignSessionStore,
79
+ foreignSessionInfoToSessionInfo,
80
+ foreignSessionSourceName,
81
+ persistForeignSession,
82
+ } from "./session/foreign-session-import";
83
+ import type { ForeignSessionInfo, ForeignSessionSource, ForeignSessionStore } from "./session/foreign-session-store";
77
84
  import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
78
85
  import { SessionManager } from "./session/session-manager";
79
86
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
@@ -83,14 +90,7 @@ import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
83
90
  import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
84
91
  import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
85
92
  import type { LspStartupServerInfo } from "./tools";
86
- import {
87
- getChangelogPath,
88
- parseChangelog,
89
- parseChangelogVersion,
90
- readLastChangelogVersion,
91
- selectStartupChangelog,
92
- writeLastChangelogVersion,
93
- } from "./utils/changelog";
93
+ import { getChangelogPath, resolveStartupChangelogForDisplay, type StartupChangelogSelection } from "./utils/changelog";
94
94
  import { EventBus } from "./utils/event-bus";
95
95
  import { withTimeoutSignal } from "./utils/fetch-timeout";
96
96
 
@@ -408,7 +408,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
408
408
  async function runInteractiveMode(
409
409
  session: AgentSession,
410
410
  version: string,
411
- changelogMarkdown: string | undefined,
411
+ startupChangelog: StartupChangelogSelection | undefined,
412
412
  notifs: (InteractiveModeNotify | null)[],
413
413
  versionCheckPromise: Promise<string | undefined>,
414
414
  initialMessages: string[],
@@ -426,7 +426,7 @@ async function runInteractiveMode(
426
426
  const mode = new InteractiveMode(
427
427
  session,
428
428
  version,
429
- changelogMarkdown,
429
+ startupChangelog,
430
430
  setExtensionUIContext,
431
431
  lspServers,
432
432
  mcpManager,
@@ -565,6 +565,27 @@ export class SessionResolutionError extends Error {
565
565
  }
566
566
  }
567
567
 
568
+ function resolveForeignSessionSource(
569
+ parsed: Pick<Args, "continue" | "fork" | "fromClaude" | "fromCodex" | "noSession" | "resume">,
570
+ ): ForeignSessionSource | undefined {
571
+ if (parsed.fromClaude && parsed.fromCodex) {
572
+ throw new SessionResolutionError("--from-claude and --from-codex cannot be used together");
573
+ }
574
+ const source = parsed.fromClaude ? "claude" : parsed.fromCodex ? "codex" : undefined;
575
+ if (!source) return undefined;
576
+ if (parsed.noSession) {
577
+ throw new SessionResolutionError(`--from-${source} requires session persistence`);
578
+ }
579
+ if (parsed.continue || parsed.resume || parsed.fork) {
580
+ throw new SessionResolutionError(`--from-${source} cannot be combined with --continue, --resume, or --fork`);
581
+ }
582
+ return source;
583
+ }
584
+
585
+ function isForeignSessionImport(parsed: Pick<Args, "fromClaude" | "fromCodex">): boolean {
586
+ return parsed.fromClaude === true || parsed.fromCodex === true;
587
+ }
588
+
568
589
  type MissingCwdMoveResult =
569
590
  | { status: "not-needed" }
570
591
  | { status: "declined" }
@@ -651,33 +672,19 @@ async function resolveScopedModels(
651
672
  );
652
673
  }
653
674
 
654
- async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
655
- if (parsed.continue || parsed.resume) {
656
- return undefined;
657
- }
658
-
659
- const lastVersion = await readLastChangelogVersion();
660
- const parsedLastVersion = parseChangelogVersion(lastVersion);
661
- if (!parsedLastVersion) {
662
- await writeLastChangelogVersion(VERSION);
663
- return undefined;
664
- }
665
- if (lastVersion === VERSION) {
666
- // Steady state: user already saw the current version's changelog. Skip the file read + parse.
675
+ async function getChangelogForDisplay(
676
+ parsed: Args,
677
+ mode: SettingValue<"startup.changelogMode">,
678
+ ): Promise<StartupChangelogSelection | undefined> {
679
+ if (parsed.continue || parsed.resume || isForeignSessionImport(parsed)) {
667
680
  return undefined;
668
681
  }
669
682
 
670
- const changelogPath = getChangelogPath();
671
- const entries = await parseChangelog(changelogPath);
672
- const startupChangelog = selectStartupChangelog(entries, lastVersion, VERSION);
673
- if (startupChangelog.persistCurrentVersion) {
674
- await writeLastChangelogVersion(VERSION);
675
- }
676
- if (startupChangelog.markdown) {
677
- return startupChangelog.markdown;
678
- }
679
-
680
- return undefined;
683
+ return resolveStartupChangelogForDisplay({
684
+ mode,
685
+ currentVersion: VERSION,
686
+ changelogPath: getChangelogPath(),
687
+ });
681
688
  }
682
689
 
683
690
  const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -852,6 +859,7 @@ export async function buildSessionOptions(
852
859
  cwd: parsed.cwd ?? getProjectDir(),
853
860
  autoApprove: parsed.autoApprove ?? false,
854
861
  };
862
+ const restoringSession = Boolean(parsed.continue || parsed.resume || isForeignSessionImport(parsed));
855
863
  const cliDirs = parsed.addDir ?? [];
856
864
  const settingsDirs = activeSettings.get("workspace.additionalDirectories");
857
865
  if (cliDirs.length > 0 || settingsDirs.length > 0) {
@@ -882,7 +890,7 @@ export async function buildSessionOptions(
882
890
  options.providerPromptCacheKeySource = "explicit";
883
891
  } else {
884
892
  const header = sessionManager?.getHeader();
885
- const scopedModelOverride = scopedModels.length > 0 && !parsed.continue && !parsed.resume;
893
+ const scopedModelOverride = scopedModels.length > 0 && !restoringSession;
886
894
  const forkCacheShapeChanged =
887
895
  scopedModelOverride ||
888
896
  parsed.model !== undefined ||
@@ -939,7 +947,7 @@ export async function buildSessionOptions(
939
947
  options.thinkingLevel = resolved.thinkingLevel;
940
948
  }
941
949
  }
942
- } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
950
+ } else if (scopedModels.length > 0 && !restoringSession) {
943
951
  const remembered = activeSettings.getModelRole("default");
944
952
  if (remembered) {
945
953
  const rememberedSpec = resolveModelRoleValue(
@@ -1042,8 +1050,7 @@ export async function buildSessionOptions(
1042
1050
  // thinking suffix) after extensions register; seeding the fallback
1043
1051
  // scoped model's level here would override it in createAgentSession.
1044
1052
  !deferredDefaultRole &&
1045
- !parsed.continue &&
1046
- !parsed.resume
1053
+ !restoringSession
1047
1054
  ) {
1048
1055
  options.thinkingLevel = scopedModels[0].thinkingLevel;
1049
1056
  }
@@ -1119,6 +1126,7 @@ interface RunRootCommandDependencies {
1119
1126
  discoverAuthStorage?: typeof discoverAuthStorage;
1120
1127
  selectSession?: typeof selectSession;
1121
1128
  runAcpMode?: RunAcpMode;
1129
+ createForeignSessionStore?: (source: ForeignSessionSource) => ForeignSessionStore;
1122
1130
  settings?: Settings;
1123
1131
  forceSetupWizard?: boolean;
1124
1132
  }
@@ -1282,19 +1290,77 @@ export async function runRootCommand(
1282
1290
  // id from UUID-shaped values owned by later extension flags.
1283
1291
  normalizeContinueSessionArgs(parsedArgs, rawArgs);
1284
1292
 
1285
- // Create session manager based on CLI flags. SessionResolutionError signals a
1286
- // user-facing failure (unknown --resume/--fork id, non-interactive fork
1287
- // prompt, --fork with --no-session): print + exit cleanly instead of letting
1288
- // it surface as `[Uncaught Exception]` (see issue #2084).
1293
+ // Resolve native resume/fork flags or import one foreign transcript into a
1294
+ // fresh persisted OMP session before constructing the AgentSession.
1289
1295
  let sessionManager: SessionManager | undefined;
1296
+ let foreignSource: ForeignSessionSource | undefined;
1290
1297
  try {
1291
- sessionManager = await logger.time(
1292
- "createSessionManager",
1293
- createSessionManager,
1294
- parsedArgs,
1295
- cwd,
1296
- settingsInstance,
1297
- );
1298
+ foreignSource = resolveForeignSessionSource(parsedArgs);
1299
+ if (foreignSource) {
1300
+ if (isProtocolMode) {
1301
+ throw new SessionResolutionError(`--from-${foreignSource} is not supported in ${mode} mode`);
1302
+ }
1303
+ const sourceName = foreignSessionSourceName(foreignSource);
1304
+ const store = (deps.createForeignSessionStore ?? createForeignSessionStore)(foreignSource);
1305
+ let foreignSessions: ForeignSessionInfo[];
1306
+ try {
1307
+ foreignSessions = await logger.time(`list${sourceName}Sessions`, () => store.list());
1308
+ } catch (error) {
1309
+ const message = error instanceof Error ? error.message : String(error);
1310
+ throw new SessionResolutionError(`Failed to list ${sourceName} sessions: ${message}`);
1311
+ }
1312
+ if (foreignSessions.length === 0) {
1313
+ writeStartupNotice(parsedArgs, `${chalk.dim(`No ${sourceName} sessions found`)}\n`);
1314
+ stopStartupWatchdog();
1315
+ process.exit(0);
1316
+ }
1317
+ const choices = foreignSessions.map(foreignSessionInfoToSessionInfo);
1318
+ pauseStartupWatchdog();
1319
+ let selected: SessionInfo | null;
1320
+ try {
1321
+ selected = await logger.time(`select${sourceName}Session`, deps.selectSession ?? selectSession, choices, {
1322
+ title: `Import ${sourceName} Session`,
1323
+ scopeLabel: false,
1324
+ showCwd: true,
1325
+ allowDelete: false,
1326
+ allowGlobalScope: false,
1327
+ historySearch: false,
1328
+ });
1329
+ } finally {
1330
+ resumeStartupWatchdog();
1331
+ }
1332
+ if (!selected) {
1333
+ writeStartupNotice(parsedArgs, `${chalk.dim(`No ${sourceName} session selected`)}\n`);
1334
+ stopStartupWatchdog();
1335
+ process.exit(0);
1336
+ }
1337
+ const foreignSession = foreignSessions.find(
1338
+ session => session.id === selected.id && session.path === selected.path,
1339
+ );
1340
+ if (!foreignSession) {
1341
+ throw new SessionResolutionError(`Selected ${sourceName} session is no longer available`);
1342
+ }
1343
+ try {
1344
+ sessionManager = await logger.time(
1345
+ `import${sourceName}Session`,
1346
+ persistForeignSession,
1347
+ store,
1348
+ foreignSession,
1349
+ { fallbackCwd: cwd, sessionDir: parsedArgs.sessionDir },
1350
+ );
1351
+ } catch (error) {
1352
+ const message = error instanceof Error ? error.message : String(error);
1353
+ throw new SessionResolutionError(`Failed to import ${sourceName} session: ${message}`);
1354
+ }
1355
+ } else {
1356
+ sessionManager = await logger.time(
1357
+ "createSessionManager",
1358
+ createSessionManager,
1359
+ parsedArgs,
1360
+ cwd,
1361
+ settingsInstance,
1362
+ );
1363
+ }
1298
1364
  } catch (error: unknown) {
1299
1365
  if (error instanceof SessionResolutionError) {
1300
1366
  process.stderr.write(`${chalk.red(`Error: ${error.message}`)}\n`);
@@ -1306,7 +1372,7 @@ export async function runRootCommand(
1306
1372
  throw error;
1307
1373
  }
1308
1374
 
1309
- if (typeof parsedArgs.resume === "string" && sessionManager) {
1375
+ if ((typeof parsedArgs.resume === "string" || foreignSource) && sessionManager) {
1310
1376
  const previousCwd = cwd;
1311
1377
  cwd = await switchToResumedProject(sessionManager.getCwd(), settingsInstance, pluginPreloadPromise);
1312
1378
  if (cwd !== previousCwd) {
@@ -1372,7 +1438,7 @@ export async function runRootCommand(
1372
1438
  sessionManager = await SessionManager.open(selected.path);
1373
1439
  }
1374
1440
 
1375
- if (sessionManager && (parsedArgs.continue || parsedArgs.resume || parsedArgs.fork)) {
1441
+ if (sessionManager && (parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource)) {
1376
1442
  const pendingToolWarning = describePendingToolCalls(sessionManager.getBranch());
1377
1443
  if (pendingToolWarning) {
1378
1444
  logger.warn("Resumed session has pending tool calls", {
@@ -1515,7 +1581,7 @@ export async function runRootCommand(
1515
1581
  const showStartupSplash = shouldShowStartupSplash({
1516
1582
  configured: settingsInstance.get("startup.showSplash"),
1517
1583
  isInteractive,
1518
- resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1584
+ resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource),
1519
1585
  quiet: settingsInstance.get("startup.quiet"),
1520
1586
  timing: Boolean($env.PI_TIMING),
1521
1587
  stdinIsTTY: process.stdin.isTTY,
@@ -1580,7 +1646,12 @@ export async function runRootCommand(
1580
1646
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
1581
1647
  } else if (isInteractive) {
1582
1648
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1583
- const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
1649
+ const startupChangelog = await logger.time(
1650
+ "main:getChangelogForDisplay",
1651
+ getChangelogForDisplay,
1652
+ parsedArgs,
1653
+ settingsInstance.get("startup.changelogMode"),
1654
+ );
1584
1655
 
1585
1656
  const modelScopeNotification = buildModelScopeNotification(
1586
1657
  scopedModels,
@@ -1605,14 +1676,14 @@ export async function runRootCommand(
1605
1676
  await runInteractiveMode(
1606
1677
  session,
1607
1678
  VERSION,
1608
- changelogMarkdown,
1679
+ startupChangelog,
1609
1680
  notifs,
1610
1681
  versionCheckPromise,
1611
1682
  initialArgs.messages,
1612
1683
  setToolUIContext,
1613
1684
  lspServers,
1614
1685
  mcpManager,
1615
- Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1686
+ Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource),
1616
1687
  deps.forceSetupWizard === true,
1617
1688
  showStartupSplash,
1618
1689
  eventBus,