@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6

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 (143) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/cli.js +3616 -3676
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/exec/bash-executor.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  15. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
  17. package/dist/types/launch/spawn-options.d.ts +10 -0
  18. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  19. package/dist/types/modes/components/status-line/component.d.ts +10 -3
  20. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  21. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  22. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  23. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  24. package/dist/types/modes/components/usage-row.d.ts +1 -1
  25. package/dist/types/modes/interactive-mode.d.ts +2 -0
  26. package/dist/types/modes/print-mode.d.ts +4 -0
  27. package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
  28. package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
  29. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  30. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  31. package/dist/types/modes/setup-wizard/index.d.ts +1 -1
  32. package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
  33. package/dist/types/modes/types.d.ts +2 -0
  34. package/dist/types/sdk.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +42 -6
  36. package/dist/types/session/messages.d.ts +6 -0
  37. package/dist/types/session/session-paths.d.ts +21 -4
  38. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  39. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  40. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  41. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  42. package/dist/types/tools/gh.d.ts +5 -3
  43. package/dist/types/tools/hub/index.d.ts +1 -1
  44. package/dist/types/tools/hub/jobs.d.ts +1 -0
  45. package/dist/types/tools/hub/types.d.ts +2 -0
  46. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  47. package/dist/types/tools/xdev.d.ts +5 -3
  48. package/dist/types/utils/git.d.ts +33 -0
  49. package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
  50. package/dist/types/vibe/runtime.d.ts +23 -0
  51. package/dist/types/web/search/providers/codex.d.ts +1 -1
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +150 -0
  54. package/src/advisor/advise-tool.ts +4 -0
  55. package/src/advisor/runtime.ts +38 -14
  56. package/src/async/job-manager.ts +3 -0
  57. package/src/cli/bench-cli.ts +8 -2
  58. package/src/cli/dry-balance-cli.ts +1 -0
  59. package/src/config/model-registry.ts +89 -8
  60. package/src/config/model-resolver.ts +78 -14
  61. package/src/config/models-config-schema.ts +1 -0
  62. package/src/config/settings.ts +3 -1
  63. package/src/dap/client.ts +168 -1
  64. package/src/dap/config.ts +51 -1
  65. package/src/dap/session.ts +575 -234
  66. package/src/dap/types.ts +6 -5
  67. package/src/discovery/agents.ts +2 -2
  68. package/src/exec/bash-executor.ts +68 -8
  69. package/src/extensibility/extensions/load-errors.ts +13 -0
  70. package/src/extensibility/extensions/runner.ts +6 -4
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  72. package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
  73. package/src/launch/broker.ts +34 -31
  74. package/src/launch/client.ts +7 -1
  75. package/src/launch/spawn-options.test.ts +31 -0
  76. package/src/launch/spawn-options.ts +17 -0
  77. package/src/lsp/types.ts +5 -1
  78. package/src/main.ts +25 -4
  79. package/src/mcp/transports/stdio.test.ts +9 -1
  80. package/src/modes/components/ask-dialog.ts +137 -73
  81. package/src/modes/components/chat-transcript-builder.ts +10 -1
  82. package/src/modes/components/status-line/component.ts +57 -3
  83. package/src/modes/components/status-line/segments.ts +20 -3
  84. package/src/modes/components/status-line/types.ts +3 -1
  85. package/src/modes/components/tool-execution.ts +5 -0
  86. package/src/modes/components/transcript-container.ts +23 -122
  87. package/src/modes/components/tree-selector.ts +10 -4
  88. package/src/modes/components/usage-row.ts +17 -1
  89. package/src/modes/controllers/command-controller.ts +41 -1
  90. package/src/modes/controllers/event-controller.ts +7 -3
  91. package/src/modes/controllers/input-controller.ts +1 -1
  92. package/src/modes/controllers/selector-controller.ts +29 -8
  93. package/src/modes/interactive-mode.ts +102 -60
  94. package/src/modes/noninteractive-dispose.test.ts +14 -1
  95. package/src/modes/print-mode.ts +81 -9
  96. package/src/modes/rpc/rpc-client.ts +76 -35
  97. package/src/modes/rpc/rpc-frame.ts +156 -0
  98. package/src/modes/rpc/rpc-input.ts +38 -0
  99. package/src/modes/rpc/rpc-mode.ts +11 -4
  100. package/src/modes/setup-wizard/index.ts +2 -0
  101. package/src/modes/setup-wizard/scenes/model.ts +134 -0
  102. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  103. package/src/modes/types.ts +2 -0
  104. package/src/modes/utils/ui-helpers.ts +9 -3
  105. package/src/prompts/system/workflow-notice.md +89 -74
  106. package/src/prompts/tools/browser.md +3 -2
  107. package/src/prompts/tools/debug.md +2 -7
  108. package/src/prompts/tools/github.md +6 -1
  109. package/src/prompts/tools/hub.md +4 -2
  110. package/src/prompts/tools/task-async-contract.md +1 -0
  111. package/src/prompts/tools/task.md +9 -2
  112. package/src/sdk.ts +38 -6
  113. package/src/session/agent-session.ts +484 -188
  114. package/src/session/messages.test.ts +91 -0
  115. package/src/session/messages.ts +248 -110
  116. package/src/session/session-manager.ts +34 -3
  117. package/src/session/session-paths.ts +38 -9
  118. package/src/slash-commands/builtin-registry.ts +1 -0
  119. package/src/task/executor.ts +104 -33
  120. package/src/task/index.ts +46 -9
  121. package/src/task/worktree.ts +10 -0
  122. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  123. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  124. package/src/tools/browser/cmux/socket-client.ts +139 -3
  125. package/src/tools/browser/run-cancellation.ts +4 -0
  126. package/src/tools/browser/tab-protocol.ts +2 -0
  127. package/src/tools/browser/tab-supervisor.ts +21 -11
  128. package/src/tools/browser/tab-worker.ts +199 -33
  129. package/src/tools/debug.ts +3 -0
  130. package/src/tools/gh.ts +40 -2
  131. package/src/tools/hub/index.ts +4 -1
  132. package/src/tools/hub/jobs.ts +42 -1
  133. package/src/tools/hub/types.ts +2 -0
  134. package/src/tools/tool-timeouts.ts +1 -1
  135. package/src/tools/xdev.ts +11 -4
  136. package/src/tools/yield.ts +4 -1
  137. package/src/utils/git.ts +237 -0
  138. package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
  139. package/src/vibe/__tests__/token-rate.test.ts +96 -0
  140. package/src/vibe/runtime.ts +50 -0
  141. package/src/web/search/index.ts +9 -5
  142. package/src/web/search/providers/codex.ts +195 -99
  143. /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
package/src/dap/types.ts CHANGED
@@ -484,10 +484,9 @@ export interface DapAdapterConfig {
484
484
  launchDefaults?: Record<string, unknown>;
485
485
  attachDefaults?: Record<string, unknown>;
486
486
  /** "stdio" (default): communicate via stdin/stdout pipes.
487
- * "socket": adapter uses a network socket instead of stdio.
488
- * On Linux, connects via a unix domain socket.
489
- * On macOS, the adapter dials into a local TCP listener (--client-addr). */
490
- connectMode?: "stdio" | "socket";
487
+ * "socket": adapter-specific socket launch (currently Delve).
488
+ * "tcp": spawn a DAP server with `${port}` substituted in `args`, then connect to it. */
489
+ connectMode?: "stdio" | "socket" | "tcp";
491
490
  /** When true, the adapter accepts a directory as the launch `program`
492
491
  * (e.g. dlv treats it as a Go package path). When false/undefined, the
493
492
  * debug tool rejects directory programs upfront. */
@@ -504,7 +503,7 @@ export interface DapResolvedAdapter {
504
503
  rootMarkers: string[];
505
504
  launchDefaults: Record<string, unknown>;
506
505
  attachDefaults: Record<string, unknown>;
507
- connectMode: "stdio" | "socket";
506
+ connectMode: "stdio" | "socket" | "tcp";
508
507
  acceptsDirectoryProgram: boolean;
509
508
  }
510
509
 
@@ -581,6 +580,8 @@ export interface DapSessionSummary {
581
580
  outputTruncated: boolean;
582
581
  exitCode?: number;
583
582
  needsConfigurationDone: boolean;
583
+ parentSessionId?: string;
584
+ childSessionIds?: string[];
584
585
  }
585
586
 
586
587
  export interface DapContinueOutcome {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Agents (standard) Provider
2
+ * Agent Dirs (.agent/.agents) Provider
3
3
  *
4
4
  * Loads skills, rules, prompts, commands, context files, and system prompts
5
5
  * from .agent/ and .agents/ directories at both user (~/) and project levels.
@@ -24,7 +24,7 @@ import {
24
24
  } from "./helpers";
25
25
 
26
26
  const PROVIDER_ID = "agents";
27
- const DISPLAY_NAME = "Agents (standard)";
27
+ const DISPLAY_NAME = "Agent Dirs (.agent/.agents)";
28
28
  const PRIORITY = 70;
29
29
  const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
30
30
 
@@ -150,9 +150,56 @@ function isBashShell(shell: string): boolean {
150
150
  return basename.includes("bash");
151
151
  }
152
152
 
153
+ const UNSUPPORTED_UNQUOTED_CD_CHARS = "\\$`;&|<>(){}*?[]!#\"'";
154
+
155
+ function hasUnsupportedUnquotedCdSyntax(value: string): boolean {
156
+ for (const char of value) {
157
+ if (/\s/.test(char) || UNSUPPORTED_UNQUOTED_CD_CHARS.includes(char)) return true;
158
+ }
159
+ return false;
160
+ }
161
+
162
+ export function isPersistentShellCdCommand(command: string): boolean {
163
+ if (/[\r\n]/.test(command)) return false;
164
+
165
+ const trimmed = command.trim();
166
+ if (trimmed === "cd") return true;
167
+ if (!trimmed.startsWith("cd") || !/[ \t]/.test(trimmed[2] ?? "")) return false;
168
+
169
+ let rest = trimmed.slice(2).trim();
170
+ if (rest === "" || rest === "--") return true;
171
+
172
+ let hasOptionTerminator = false;
173
+ if (/^--[ \t]/.test(rest)) {
174
+ hasOptionTerminator = true;
175
+ rest = rest.slice(2).trimStart();
176
+ }
177
+ if (rest === "") return true;
178
+
179
+ const quote = rest[0];
180
+ let target: string;
181
+ let quoted = false;
182
+ if (quote === `"` || quote === "'") {
183
+ if (rest.length < 2 || rest[rest.length - 1] !== quote) return false;
184
+ target = rest.slice(1, -1);
185
+ if (target.includes(quote)) return false;
186
+ if (quote === `"` && /[\\$`\r\n]/.test(target)) return false;
187
+ quoted = true;
188
+ } else {
189
+ if (hasUnsupportedUnquotedCdSyntax(rest)) return false;
190
+ target = rest;
191
+ }
192
+
193
+ if (target === "") return false;
194
+ if (/^[+-]\d+$/.test(target)) return false;
195
+ if (!hasOptionTerminator && target.startsWith("-") && target !== "-") return false;
196
+ if (!quoted && target.startsWith("~") && target !== "~" && !target.startsWith("~/")) return false;
197
+ return true;
198
+ }
199
+
153
200
  function needsInteractiveShellArg(shell: string): boolean {
154
201
  const basename = shellBasename(shell);
155
- return basename.includes("zsh");
202
+ return basename.includes("zsh") || basename.includes("fish");
156
203
  }
157
204
 
158
205
  function supportsAutoUserShell(shell: string): boolean {
@@ -165,19 +212,31 @@ function hasInteractiveShellArg(args: string[]): boolean {
165
212
  }
166
213
 
167
214
  function ensureInteractiveShellArgs(shell: string, args: string[]): string[] {
168
- if (!needsInteractiveShellArg(shell) || hasInteractiveShellArg(args)) return args;
215
+ if (!needsInteractiveShellArg(shell)) return args;
216
+
217
+ // fish sources the same config files (config.fish + conf.d) for interactive
218
+ // shells as for login shells, so the inherited `-l` adds nothing — it only
219
+ // marks the shell as login, firing `status is-login` blocks in user config
220
+ // (agent/keychain setup, path mutation) on every `!` command. zsh keeps `-l`
221
+ // because .zprofile is login-only. Args originate from procmgr's
222
+ // getShellArgs(), so login only ever appears as a standalone `-l`/`--login`.
223
+ const effectiveArgs = shellBasename(shell).includes("fish")
224
+ ? args.filter(arg => arg !== "-l" && arg !== "--login")
225
+ : args;
226
+
227
+ if (hasInteractiveShellArg(effectiveArgs)) return effectiveArgs;
169
228
 
170
- const commandIndex = args.findIndex(arg => arg === "-c" || arg === "--command");
229
+ const commandIndex = effectiveArgs.findIndex(arg => arg === "-c" || arg === "--command");
171
230
  if (commandIndex !== -1) {
172
- return [...args.slice(0, commandIndex), "-i", ...args.slice(commandIndex)];
231
+ return [...effectiveArgs.slice(0, commandIndex), "-i", ...effectiveArgs.slice(commandIndex)];
173
232
  }
174
233
 
175
- const compactCommandIndex = args.findIndex(arg => /^-[^-]*c[^-]*$/.test(arg));
234
+ const compactCommandIndex = effectiveArgs.findIndex(arg => /^-[^-]*c[^-]*$/.test(arg));
176
235
  if (compactCommandIndex !== -1) {
177
- return args.map((arg, index) => (index === compactCommandIndex ? arg.replace("c", "ic") : arg));
236
+ return effectiveArgs.map((arg, index) => (index === compactCommandIndex ? arg.replace("c", "ic") : arg));
178
237
  }
179
238
 
180
- return [...args, "-i"];
239
+ return [...effectiveArgs, "-i"];
181
240
  }
182
241
 
183
242
  function quoteShellArg(value: string): string {
@@ -224,8 +283,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
224
283
 
225
284
  // Apply command prefix if configured
226
285
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
286
+ const runCdInPersistentShell = options?.useUserShell === true && !prefix && isPersistentShellCdCommand(command);
227
287
  const finalCommand =
228
- options?.useUserShell === true && !bashShell
288
+ options?.useUserShell === true && !bashShell && !runCdInPersistentShell
229
289
  ? buildUserShellCommand(shell, args, prefixedCommand)
230
290
  : prefixedCommand;
231
291
 
@@ -0,0 +1,13 @@
1
+ import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
2
+ import type { LoadExtensionsResult } from "./types";
3
+
4
+ /** Formats extension load failures for user-visible startup diagnostics. */
5
+ export function formatExtensionLoadNotifications(errors: LoadExtensionsResult["errors"]): string[] {
6
+ const messages: string[] = [];
7
+ for (const { path, error } of errors) {
8
+ const displayPath = truncateToWidth(replaceTabs(shortenPath(path)), TRUNCATE_LENGTHS.CONTENT);
9
+ const displayError = truncateToWidth(replaceTabs(error.replace(/\s+/g, " ").trim()), TRUNCATE_LENGTHS.LONG);
10
+ messages.push(`Failed to load extension ${displayPath}: ${displayError}`);
11
+ }
12
+ return messages;
13
+ }
@@ -534,8 +534,9 @@ export class ExtensionRunner {
534
534
  return undefined;
535
535
  }
536
536
 
537
- createContext(): ExtensionContext {
538
- const getModel = this.#getModel;
537
+ /** Creates an extension context, optionally scoped to a provider request model. */
538
+ createContext(model?: Model): ExtensionContext {
539
+ const getModel = model ? () => model : this.#getModel;
539
540
  return {
540
541
  ui: this.#uiContext,
541
542
  getContextUsage: () => this.#getContextUsageFn(),
@@ -965,8 +966,9 @@ export class ExtensionRunner {
965
966
  return currentMessages;
966
967
  }
967
968
 
968
- async emitBeforeProviderRequest(payload: unknown): Promise<BeforeProviderRequestEventResult> {
969
- const ctx = this.createContext();
969
+ /** Runs request payload hooks with the model used for that provider request. */
970
+ async emitBeforeProviderRequest(payload: unknown, model?: Model): Promise<BeforeProviderRequestEventResult> {
971
+ const ctx = this.createContext(model);
970
972
  let currentPayload = payload;
971
973
 
972
974
  for (const ext of this.extensions) {
@@ -22,8 +22,10 @@ import {
22
22
  getAgentDbPath,
23
23
  getAgentDir,
24
24
  getProjectDir,
25
+ isCompiledBinary,
25
26
  parseFrontmatter as parseOmpFrontmatter,
26
27
  } from "@oh-my-pi/pi-utils";
28
+ import { getPackageDir as getOmpPackageDir } from "../config";
27
29
  import type { PromptTemplate } from "../config/prompt-templates";
28
30
  import { type SettingPath, Settings } from "../config/settings";
29
31
  import { EditTool } from "../edit";
@@ -1334,6 +1336,30 @@ export function readStoredCredential(provider: string): AuthCredential | undefin
1334
1336
  return storage.get(provider);
1335
1337
  }
1336
1338
 
1339
+ // Pi SDK path helpers. `export * from "../index"` above only forwards
1340
+ // `getAgentDir`; `getProjectDir` (a `@oh-my-pi/pi-utils` helper) and
1341
+ // `getPackageDir` are absent from that barrel, so legacy extensions importing
1342
+ // either fail Bun's static export check during validation (issue #5968).
1343
+ export { getProjectDir } from "@oh-my-pi/pi-utils";
1344
+
1345
+ /**
1346
+ * Coding-agent package install directory, matching pi's string-valued
1347
+ * `getPackageDir()` contract (extensions do `path.join(getPackageDir(), ...)`
1348
+ * to auto-allow bundled docs/resources).
1349
+ *
1350
+ * omp's canonical `getPackageDir()` (`../config`) returns `undefined` inside a
1351
+ * `bun --compile` binary — `import.meta.dir` is `/$bunfs/root` and no owning
1352
+ * `package.json` exists (issue #1423). Returning `undefined` there would crash
1353
+ * every legacy `path.join(getPackageDir(), ...)` at runtime in the shipped
1354
+ * binary, the primary distribution. So fall back to the executable's own
1355
+ * directory in compiled mode, where the binary *is* the install root. The
1356
+ * `PI_PACKAGE_DIR` override and dev/source/npm-dist walk-up still win via the
1357
+ * canonical helper.
1358
+ */
1359
+ export function getPackageDir(): string {
1360
+ return getOmpPackageDir() ?? (isCompiledBinary() ? path.dirname(process.execPath) : process.cwd());
1361
+ }
1362
+
1337
1363
  export * from "../index";
1338
1364
  export { formatBytes as formatSize } from "../tools/render-utils";
1339
1365
  export { Type } from "./typebox";
@@ -32,6 +32,16 @@ type BundledModule = Readonly<Record<string, unknown>>;
32
32
  type BundledModules = Readonly<Record<string, BundledModule>>;
33
33
  type BundledModuleLoaders = Readonly<Record<string, () => Promise<BundledModule>>>;
34
34
 
35
+ interface LegacyPiResolveResult {
36
+ path: string;
37
+ namespace?: string;
38
+ }
39
+
40
+ interface BundledVirtualResolveResult {
41
+ path: string;
42
+ namespace: typeof BUNDLED_VIRTUAL_NAMESPACE;
43
+ }
44
+
35
45
  const loadedBundledModules: Record<string, BundledModule> = {};
36
46
  let bundledModuleLoadersPromise: Promise<BundledModuleLoaders> | null = null;
37
47
 
@@ -72,6 +82,25 @@ function isBundledVirtualSpecifier(value: string): boolean {
72
82
  return value.startsWith(BUNDLED_VIRTUAL_SCHEME);
73
83
  }
74
84
 
85
+ function toLegacyPiResolveResult(resolvedPath: string): LegacyPiResolveResult {
86
+ if (isBundledVirtualSpecifier(resolvedPath)) {
87
+ const registryKey = resolvedPath.slice(BUNDLED_VIRTUAL_SCHEME.length);
88
+ return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
89
+ }
90
+ return { path: resolvedPath };
91
+ }
92
+
93
+ /** Maps a bundled virtual specifier or registry key to Bun's plugin namespace shape. */
94
+ export function resolveBundledVirtualSpecifier(specifier: string): BundledVirtualResolveResult {
95
+ const registryKey = isBundledVirtualSpecifier(specifier)
96
+ ? specifier.slice(BUNDLED_VIRTUAL_SCHEME.length)
97
+ : specifier;
98
+ if (!registryKey) {
99
+ throw new Error("omp:legacy-pi-shim: bundled virtual specifier has no registry key");
100
+ }
101
+ return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
102
+ }
103
+
75
104
  /**
76
105
  * Build a synthetic ES module for one live bundled namespace. Every export
77
106
  * reads through the global bridge; no bunfs path or copied package is involved.
@@ -1611,7 +1640,7 @@ function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
1611
1640
  return "js";
1612
1641
  }
1613
1642
 
1614
- function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { path: string } | undefined {
1643
+ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): LegacyPiResolveResult | undefined {
1615
1644
  const remappedSpecifier = remapLegacyPiSpecifier(args.path);
1616
1645
  if (!remappedSpecifier) {
1617
1646
  return undefined;
@@ -1620,7 +1649,7 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
1620
1649
  // Primary: resolve the canonical @oh-my-pi/* specifier from the host binary
1621
1650
  // location. Works in dev mode and in source-link installs.
1622
1651
  try {
1623
- return { path: resolveCanonicalPiSpecifier(remappedSpecifier) };
1652
+ return toLegacyPiResolveResult(resolveCanonicalPiSpecifier(remappedSpecifier));
1624
1653
  } catch {
1625
1654
  // Fallback for compiled binary mode: the bundled packages live inside
1626
1655
  // /$bunfs/root and aren't reachable by filesystem resolution. Prefer the
@@ -1630,10 +1659,10 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
1630
1659
  // @earendil-works peer deps.
1631
1660
  const importerDir = path.dirname(args.importer);
1632
1661
  try {
1633
- return { path: Bun.resolveSync(remappedSpecifier, importerDir) };
1662
+ return toLegacyPiResolveResult(Bun.resolveSync(remappedSpecifier, importerDir));
1634
1663
  } catch {
1635
1664
  try {
1636
- return { path: Bun.resolveSync(args.path, importerDir) };
1665
+ return toLegacyPiResolveResult(Bun.resolveSync(args.path, importerDir));
1637
1666
  } catch {
1638
1667
  return undefined;
1639
1668
  }
@@ -1641,8 +1670,8 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
1641
1670
  }
1642
1671
  }
1643
1672
 
1644
- function resolveTypeBoxSpecifier(): { path: string } | undefined {
1645
- return TYPEBOX_SHIM_PATH ? { path: TYPEBOX_SHIM_PATH } : undefined;
1673
+ function resolveTypeBoxSpecifier(): LegacyPiResolveResult | undefined {
1674
+ return TYPEBOX_SHIM_PATH ? toLegacyPiResolveResult(TYPEBOX_SHIM_PATH) : undefined;
1646
1675
  }
1647
1676
 
1648
1677
  export function installLegacyPiSpecifierShim(): void {
@@ -1656,6 +1685,12 @@ export function installLegacyPiSpecifierShim(): void {
1656
1685
  setup(build) {
1657
1686
  build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
1658
1687
  build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
1688
+ build.onResolve({ filter: /^omp-legacy-pi-bundled:.+$/, namespace: "file" }, args =>
1689
+ resolveBundledVirtualSpecifier(args.path),
1690
+ );
1691
+ build.onResolve({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, args =>
1692
+ resolveBundledVirtualSpecifier(args.path),
1693
+ );
1659
1694
  // Compiled mode serves `omp-legacy-pi-bundled:<key>` imports from
1660
1695
  // live host module references. No bunfs path leaves this loader.
1661
1696
  build.onLoad({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, async args => {
@@ -4,6 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
6
6
  import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
7
+ import { hostHasInheritableConsole } from "../eval/py/spawn-options";
7
8
  import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
8
9
  import { workerEnvFromParent } from "../subprocess/worker-client";
9
10
  import { daemonBrokerEndpoint } from "./paths";
@@ -24,6 +25,7 @@ import {
24
25
  parseDaemonSpec,
25
26
  parseDaemonWireRequest,
26
27
  } from "./protocol";
28
+ import { resolveDaemonSpawnOptions } from "./spawn-options";
27
29
 
28
30
  const DEFAULT_IDLE_GRACE_MS = 3_000;
29
31
  const MAX_REQUEST_BYTES = 1024 * 1024;
@@ -36,6 +38,10 @@ const PID_FILE = "broker.pid";
36
38
  const META_FILE = "meta.json";
37
39
  const LOG_FILE = "output.log";
38
40
  const PREVIOUS_LOG_FILE = "output.previous.log";
41
+ const DAEMON_SPAWN_OPTIONS = resolveDaemonSpawnOptions({
42
+ platform: process.platform,
43
+ hostHasInheritableConsole: hostHasInheritableConsole(),
44
+ });
39
45
 
40
46
  const SIGNAL_NUMBER: Record<DaemonSignal, number> = {
41
47
  SIGINT: os.constants.signals.SIGINT,
@@ -537,6 +543,15 @@ class DaemonBroker {
537
543
  if (error) record.log?.append(`PTY output error: ${error.message}\n`);
538
544
  if (chunk) this.#onOutput(record, generation, chunk);
539
545
  };
546
+ const started = Promise.withResolvers<number | undefined>();
547
+ const onStart = (error: Error | null, pid: number): void => {
548
+ if (error) {
549
+ record.log?.append(`PTY startup callback failed: ${error.message}\n`);
550
+ started.resolve(undefined);
551
+ return;
552
+ }
553
+ started.resolve(Number.isSafeInteger(pid) && pid > 0 ? pid : undefined);
554
+ };
540
555
  let run: Promise<PtyRunResult>;
541
556
  if (process.platform === "win32") {
542
557
  run = session.startArgv(
@@ -546,41 +561,29 @@ class DaemonBroker {
546
561
  ...options,
547
562
  },
548
563
  onChunk,
564
+ onStart,
549
565
  );
550
566
  } else {
551
- const pidPath = path.join(record.dir, "process.pid");
552
- await fs.rm(pidPath, { force: true });
553
567
  const argv = [record.spec.application, ...record.spec.args];
554
- const command = [
555
- `printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
556
- `exec ${argv.map(quoteShellArg).join(" ")}`,
557
- ].join("; ");
568
+ const command = `exec ${argv.map(quoteShellArg).join(" ")}`;
558
569
  const shell = procmgr.getShellConfig().shell;
559
- run = session.start({ command, shell, ...options }, onChunk);
570
+ run = session.start({ command, shell, ...options }, onChunk, onStart);
560
571
  }
561
- void run
562
- .then(result => this.#onPtyExit(record, generation, result))
563
- .catch(error =>
564
- this.#settle(record, generation, undefined, error instanceof Error ? error.message : String(error)),
565
- );
572
+ void run.then(
573
+ async result => {
574
+ await this.#onPtyExit(record, generation, result);
575
+ started.resolve(undefined);
576
+ },
577
+ async error => {
578
+ await this.#settle(record, generation, undefined, error instanceof Error ? error.message : String(error));
579
+ started.resolve(undefined);
580
+ },
581
+ );
566
582
 
567
- if (process.platform === "win32") return;
568
- const pidPath = path.join(record.dir, "process.pid");
569
- const deadline = Date.now() + 5_000;
570
- const pidFile = Bun.file(pidPath);
571
- while (Date.now() < deadline && generation === record.generation) {
572
- try {
573
- const pid = Number.parseInt((await pidFile.text()).trim(), 10);
574
- if (Number.isSafeInteger(pid) && pid > 0) {
575
- record.snapshot.pid = pid;
576
- this.#persist(record);
577
- return;
578
- }
579
- } catch (error) {
580
- if (!isEnoent(error)) throw error;
581
- }
582
- if (terminalState(record.snapshot.state)) return;
583
- await Bun.sleep(20);
583
+ const pid = await started.promise;
584
+ if (pid !== undefined && generation === record.generation) {
585
+ record.snapshot.pid = pid;
586
+ this.#persist(record);
584
587
  }
585
588
  }
586
589
 
@@ -591,7 +594,7 @@ class DaemonBroker {
591
594
  stdin: "pipe",
592
595
  stdout: "pipe",
593
596
  stderr: "pipe",
594
- detached: true,
597
+ ...DAEMON_SPAWN_OPTIONS,
595
598
  });
596
599
  record.process = process;
597
600
  record.input = process.stdin;
@@ -614,7 +617,7 @@ class DaemonBroker {
614
617
  cwd: record.spec.cwd,
615
618
  env: workerEnvFromParent(record.spec.env),
616
619
  stdio: ["ignore", output.fd, output.fd],
617
- detached: true,
620
+ ...DAEMON_SPAWN_OPTIONS,
618
621
  });
619
622
  record.process = process;
620
623
  record.snapshot.pid = process.pid;
@@ -3,6 +3,7 @@ import * as net from "node:net";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
6
+ import { hostHasInheritableConsole } from "../eval/py/spawn-options";
6
7
  import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
7
8
  import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
8
9
  import {
@@ -16,10 +17,15 @@ import {
16
17
  parseDaemonRpcResult,
17
18
  parseDaemonWireResponse,
18
19
  } from "./protocol";
20
+ import { resolveDaemonSpawnOptions } from "./spawn-options";
19
21
 
20
22
  const CONNECT_TIMEOUT_MS = 10_000;
21
23
  const CONNECT_RETRY_MS = 50;
22
24
  const TOKEN_FILE = "broker.token";
25
+ const BROKER_SPAWN_OPTIONS = resolveDaemonSpawnOptions({
26
+ platform: process.platform,
27
+ hostHasInheritableConsole: hostHasInheritableConsole(),
28
+ });
23
29
 
24
30
  interface PendingRequest {
25
31
  operation: DaemonOperation;
@@ -228,7 +234,7 @@ class SocketDaemonClient implements DaemonBrokerClient {
228
234
  stdin: "ignore",
229
235
  stdout: "ignore",
230
236
  stderr: "ignore",
231
- detached: true,
237
+ ...BROKER_SPAWN_OPTIONS,
232
238
  });
233
239
  child.unref();
234
240
  }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { resolveDaemonSpawnOptions } from "./spawn-options";
3
+
4
+ describe("resolveDaemonSpawnOptions", () => {
5
+ it("hides Windows daemons when the host has no console", () => {
6
+ expect(
7
+ resolveDaemonSpawnOptions({
8
+ platform: "win32",
9
+ hostHasInheritableConsole: false,
10
+ }),
11
+ ).toEqual({ detached: false, windowsHide: true });
12
+ });
13
+
14
+ it("inherits the Windows host console instead of detaching", () => {
15
+ expect(
16
+ resolveDaemonSpawnOptions({
17
+ platform: "win32",
18
+ hostHasInheritableConsole: true,
19
+ }),
20
+ ).toEqual({ detached: false, windowsHide: false });
21
+ });
22
+
23
+ it("keeps POSIX daemons in their own session", () => {
24
+ expect(
25
+ resolveDaemonSpawnOptions({
26
+ platform: "linux",
27
+ hostHasInheritableConsole: false,
28
+ }),
29
+ ).toEqual({ detached: true });
30
+ });
31
+ });
@@ -0,0 +1,17 @@
1
+ /** Platform-specific options for the launch broker and its non-PTY children. */
2
+ export interface DaemonSpawnOptions {
3
+ detached: boolean;
4
+ windowsHide?: boolean;
5
+ }
6
+
7
+ /** Keep launch processes headless without discarding an inheritable Windows console. */
8
+ export function resolveDaemonSpawnOptions(opts: {
9
+ platform: NodeJS.Platform;
10
+ hostHasInheritableConsole: boolean;
11
+ }): DaemonSpawnOptions {
12
+ if (opts.platform !== "win32") return { detached: true };
13
+ return {
14
+ detached: false,
15
+ windowsHide: !opts.hostHasInheritableConsole,
16
+ };
17
+ }
package/src/lsp/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ptree } from "@oh-my-pi/pi-utils";
2
2
  import { type } from "arktype";
3
+ import { TOOL_TIMEOUTS } from "../tools/tool-timeouts";
3
4
 
4
5
  // =============================================================================
5
6
  // Tool Schema
@@ -14,7 +15,10 @@ export const lspSchema = type({
14
15
  query: "string?",
15
16
  new_name: "string?",
16
17
  apply: "boolean?",
17
- timeout: "number?",
18
+ "timeout?": type.number
19
+ .atLeast(TOOL_TIMEOUTS.lsp.min)
20
+ .atMost(TOOL_TIMEOUTS.lsp.max)
21
+ .describe("Timeout in seconds (default 20; range 5–300)."),
18
22
  payload: "string?",
19
23
  });
20
24
 
package/src/main.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  logger,
18
18
  normalizePathForComparison,
19
19
  postmortem,
20
+ setInteractiveHost,
20
21
  setProjectDir,
21
22
  VERSION,
22
23
  } from "@oh-my-pi/pi-utils";
@@ -49,6 +50,7 @@ import {
49
50
  resolveActiveProjectRegistryPath,
50
51
  } from "./discovery/helpers";
51
52
  import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
53
+ import { formatExtensionLoadNotifications } from "./extensibility/extensions/load-errors";
52
54
  import { ExtensionRunner } from "./extensibility/extensions/runner";
53
55
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
54
56
  import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
@@ -56,6 +58,7 @@ import { registerDaemonProjectPresence } from "./launch/presence";
56
58
  import type { MCPManager } from "./mcp";
57
59
  import { InteractiveMode } from "./modes/interactive-mode";
58
60
  import type { PrintModeOptions } from "./modes/print-mode";
61
+ import { claimRpcInput } from "./modes/rpc/rpc-input";
59
62
  import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
60
63
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
61
64
  import type { SubmittedUserInput } from "./modes/types";
@@ -97,6 +100,7 @@ type RunRpcMode = (
97
100
  session: AgentSession,
98
101
  setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
99
102
  eventBus?: EventBus,
103
+ input?: ReadableStream<Uint8Array>,
100
104
  ) => Promise<never>;
101
105
 
102
106
  export function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void {
@@ -884,8 +888,12 @@ export async function buildSessionOptions(
884
888
  if (resolved.warning) {
885
889
  process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
886
890
  }
887
- if (resolved.error) {
888
- if (!parsed.provider && !parsed.model.includes(":")) {
891
+ const matchedAfterMissingRolePattern = (resolved.configuredPatternIndex ?? 0) > 0;
892
+ if (matchedAfterMissingRolePattern) {
893
+ // Extensions may register an earlier configured role candidate.
894
+ options.modelPattern = parsed.model;
895
+ } else if (resolved.error) {
896
+ if (!parsed.provider && ((resolved.configuredPatterns?.length ?? 0) > 0 || !parsed.model.includes(":"))) {
889
897
  // Model not found in built-in registry — defer resolution to after extensions load
890
898
  // (extensions may register additional providers/models via registerProvider)
891
899
  options.modelPattern = parsed.model;
@@ -1106,6 +1114,9 @@ export async function runRootCommand(
1106
1114
  process.stderr.write(`${chalk.red("Error: @file arguments are not supported in RPC mode")}\n`);
1107
1115
  process.exit(1);
1108
1116
  }
1117
+ const mode = parsedArgs.mode || "text";
1118
+ // RPC owns stdin. Claim its singleton stream before plugin/extension discovery can load an in-process consumer.
1119
+ const rpcInput = mode === "rpc" || mode === "rpc-ui" ? claimRpcInput() : undefined;
1109
1120
 
1110
1121
  // Kick off plugin-root preload in parallel with the remaining startup work.
1111
1122
  // Awaited later (before extension/skill discovery in createAgentSession needs it).
@@ -1152,12 +1163,15 @@ export async function runRootCommand(
1152
1163
  if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
1153
1164
  Bun.env.PI_NO_TITLE = "1";
1154
1165
  }
1155
- const mode = parsedArgs.mode || "text";
1156
1166
  const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
1157
1167
  // Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
1158
1168
  const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
1159
1169
  const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
1160
1170
  const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
1171
+ // Only the interactive host renders a focusable Agent Hub / subagent session
1172
+ // tree; declare it so headless subagent optimizations (e.g. skipping replan
1173
+ // title refresh) can tell a focusable process from a print/RPC/eval one.
1174
+ setInteractiveHost(isInteractive);
1161
1175
 
1162
1176
  // Initialize discovery system with settings for provider persistence
1163
1177
  logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
@@ -1420,6 +1434,13 @@ export async function runRootCommand(
1420
1434
  };
1421
1435
  const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
1422
1436
  normalizeContinueSessionArgs(initialArgs, rawArgs);
1437
+ for (const message of formatExtensionLoadNotifications(extensionsResult.errors)) {
1438
+ if (isInteractive) {
1439
+ notifs.push({ kind: "warn", message });
1440
+ } else {
1441
+ process.stderr.write(`${chalk.yellow(`${message}\n`)}`);
1442
+ }
1443
+ }
1423
1444
  // Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
1424
1445
  // know the real extension flag set. Without this check the unrecognized
1425
1446
  // token gets silently consumed and any following positional leaks as the
@@ -1509,7 +1530,7 @@ export async function runRootCommand(
1509
1530
  // Branch-only protocol runner: keep RPC host code out of normal interactive startup.
1510
1531
  const runRpcMode: RunRpcMode = (await import("./modes/rpc/rpc-mode")).runRpcMode;
1511
1532
  stopStartupWatchdog();
1512
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus);
1533
+ await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
1513
1534
  } else if (isInteractive) {
1514
1535
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1515
1536
  const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);