@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
@@ -44,7 +44,7 @@ export function createNoOpUIContext(): HookUIContext {
44
44
  }
45
45
 
46
46
  /**
47
- * Raised by {@link withExitGuard} when a guarded callback synchronously
47
+ * Raised by {@link withHostGuard} when a guarded callback synchronously
48
48
  * attempts to terminate the host process. Callers catch this like any other
49
49
  * load-time failure so the extension/hook is skipped with a logged error
50
50
  * instead of taking the CLI down with it.
@@ -66,22 +66,46 @@ export class ExtensionExitError extends Error {
66
66
 
67
67
  type ExitAliasName = "process.exit" | "process.reallyExit";
68
68
 
69
- let exitGuardDepth = 0;
70
- let exitGuardOriginalProcessExit: typeof process.exit | null = null;
71
- let exitGuardOriginalReallyExit: typeof process.reallyExit | null = null;
69
+ /**
70
+ * stdin events a loaded module must not be allowed to leave hijacked. A
71
+ * top-level `new StdioServerTransport()` (or a bare `process.stdin.resume()`)
72
+ * inside a `~/.claude/tools` MCP server attaches a `data` consumer and puts the
73
+ * shared stdin into flowing mode; Bun delivers one `data` event to that
74
+ * consumer and the TUI's own listener (attached later in `terminal.start()`)
75
+ * then never re-arms — every keypress after the first is swallowed (#5618).
76
+ */
77
+ const HOST_GUARD_STDIN_EVENTS = ["data", "readable", "end", "close", "error"] as const;
78
+ type StdinGuardEvent = (typeof HOST_GUARD_STDIN_EVENTS)[number];
79
+ type StdinGuardListener = (...args: unknown[]) => void;
80
+
81
+ let hostGuardDepth = 0;
82
+ let hostGuardOriginalProcessExit: typeof process.exit | null = null;
83
+ let hostGuardOriginalReallyExit: typeof process.reallyExit | null = null;
84
+ let hostGuardStdinListeners: Record<StdinGuardEvent, StdinGuardListener[]> | null = null;
85
+ let hostGuardStdinWasPaused = false;
86
+ let hostGuardStdinWasRaw = false;
72
87
 
73
88
  /**
74
- * Run `fn` with hard-exit APIs patched so any synchronous attempt to terminate
75
- * the host raises {@link ExtensionExitError} instead. Restored in `finally`.
89
+ * Run `fn` with host-owned process state fenced off from third-party module
90
+ * evaluation, restored in `finally`. Guards the dynamic-import and
91
+ * factory-invocation sites that load extension / hook / tool / plugin modules
92
+ * from user directories (including Claude Code's `~/.claude/tools`, which OMP
93
+ * slurps wholesale). Two hazards are neutralized:
76
94
  *
77
- * Guards the dynamic-import and factory-invocation sites that load third-party
78
- * extension / hook modules a `process.exit(0)` or `process.reallyExit(0)` in
79
- * a stranger's script (e.g. a Codex hook script that happens to live next to
80
- * OMP-shaped modules) would otherwise kill OMP during startup with no error
81
- * surface, since `try/catch` cannot intercept a synchronous exit.
95
+ * - **Hard exit.** `process.exit(0)` / `process.reallyExit(0)` in a stranger's
96
+ * script (e.g. a CLI-shaped module with `main()` at the bottom) would kill
97
+ * OMP during startup with no error surface, since `try/catch` cannot
98
+ * intercept a synchronous exit. Both are patched to throw
99
+ * {@link ExtensionExitError} instead.
100
+ * - **stdin hijack.** A module that attaches a stdin consumer at evaluation
101
+ * time (an MCP `StdioServerTransport`, or a bare `resume()`) steals Bun's
102
+ * single stdin reader, so the TUI goes permanently deaf after one keypress
103
+ * (#5618). Any `data`/`readable`/`end`/`close`/`error` listener the module
104
+ * adds is removed, and the stream's paused and raw-mode state is restored to
105
+ * the pre-load snapshot.
82
106
  *
83
107
  * Nested and concurrent guard windows are safe: only the outermost guard
84
- * restores the real hard-exit APIs.
108
+ * snapshots and restores host state.
85
109
  */
86
110
  function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
87
111
  return (code?: number | string): never => {
@@ -89,29 +113,71 @@ function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
89
113
  };
90
114
  }
91
115
 
92
- export async function withExitGuard<T>(fn: () => Promise<T>): Promise<T> {
93
- if (exitGuardDepth === 0) {
94
- exitGuardOriginalProcessExit = process.exit;
116
+ export async function withHostGuard<T>(fn: () => Promise<T>): Promise<T> {
117
+ if (hostGuardDepth === 0) {
118
+ hostGuardOriginalProcessExit = process.exit;
95
119
  process.exit = guardedExit("process.exit") as typeof process.exit;
96
120
 
97
121
  if (typeof process.reallyExit === "function") {
98
- exitGuardOriginalReallyExit = process.reallyExit;
122
+ hostGuardOriginalReallyExit = process.reallyExit;
99
123
  process.reallyExit = guardedExit("process.reallyExit") as typeof process.reallyExit;
100
124
  }
125
+
126
+ const stdin = process.stdin;
127
+ hostGuardStdinWasPaused = stdin.isPaused();
128
+ hostGuardStdinWasRaw = stdin.isRaw ?? false;
129
+ const snapshot = {} as Record<StdinGuardEvent, StdinGuardListener[]>;
130
+ for (const event of HOST_GUARD_STDIN_EVENTS) {
131
+ snapshot[event] = stdin.rawListeners(event) as StdinGuardListener[];
132
+ }
133
+ hostGuardStdinListeners = snapshot;
101
134
  }
102
- exitGuardDepth++;
135
+ hostGuardDepth++;
103
136
  try {
104
137
  return await fn();
105
138
  } finally {
106
- exitGuardDepth--;
107
- if (exitGuardDepth === 0) {
108
- if (exitGuardOriginalProcessExit) {
109
- process.exit = exitGuardOriginalProcessExit;
110
- exitGuardOriginalProcessExit = null;
139
+ hostGuardDepth--;
140
+ if (hostGuardDepth === 0) {
141
+ if (hostGuardOriginalProcessExit) {
142
+ process.exit = hostGuardOriginalProcessExit;
143
+ hostGuardOriginalProcessExit = null;
144
+ }
145
+ if (hostGuardOriginalReallyExit) {
146
+ process.reallyExit = hostGuardOriginalReallyExit;
147
+ hostGuardOriginalReallyExit = null;
111
148
  }
112
- if (exitGuardOriginalReallyExit) {
113
- process.reallyExit = exitGuardOriginalReallyExit;
114
- exitGuardOriginalReallyExit = null;
149
+ if (hostGuardStdinListeners) {
150
+ const stdin = process.stdin;
151
+ for (const event of HOST_GUARD_STDIN_EVENTS) {
152
+ const before = hostGuardStdinListeners[event];
153
+ // Reconcile the stream back to the pre-load snapshot: drop any
154
+ // listener the module added, and reinstate any snapshot listener
155
+ // it removed (e.g. a factory calling `removeAllListeners("data")`
156
+ // would otherwise permanently strip ProcessTerminal's input
157
+ // handler, leaving the parent TUI deaf). removeAllListeners then
158
+ // re-adding in snapshot order restores both membership and order.
159
+ const current = stdin.rawListeners(event) as StdinGuardListener[];
160
+ const differs =
161
+ current.length !== before.length || current.some((listener, index) => listener !== before[index]);
162
+ if (!differs) continue;
163
+ stdin.removeAllListeners(event);
164
+ for (const listener of before) {
165
+ stdin.on(event, listener);
166
+ }
167
+ }
168
+ if (
169
+ stdin.isTTY &&
170
+ typeof stdin.setRawMode === "function" &&
171
+ (stdin.isRaw ?? false) !== hostGuardStdinWasRaw
172
+ ) {
173
+ stdin.setRawMode(hostGuardStdinWasRaw);
174
+ }
175
+ if (hostGuardStdinWasPaused && !stdin.isPaused()) {
176
+ stdin.pause();
177
+ } else if (!hostGuardStdinWasPaused && stdin.isPaused()) {
178
+ stdin.resume();
179
+ }
180
+ hostGuardStdinListeners = null;
115
181
  }
116
182
  }
117
183
  }
package/src/irc/bus.ts CHANGED
@@ -127,12 +127,31 @@ export class IrcBus {
127
127
  };
128
128
  }
129
129
 
130
+ // A `parked` recipient always needs the lifecycle to revive it — this is
131
+ // read from *this* bus's registry, so it holds for any registry. The
132
+ // mid-park / adopted checks below query the lifecycle's own state, which
133
+ // only describes the registry it manages: consult them only when the
134
+ // lifecycle owns this bus's registry, otherwise a custom-registry bus
135
+ // (fallen back to the global manager) would gate a live recipient on
136
+ // unrelated global park state. Main/non-adopted live peers skip the gate,
137
+ // and pending waiters still win without a session.
138
+ const lifecycle = this.#lifecycle();
139
+ const lifecycleOwnsRegistry = lifecycle.manages(this.#registry);
140
+ const needsLifecycleGate =
141
+ ref.status === "parked" ||
142
+ (lifecycleOwnsRegistry && (lifecycle.isParking(message.to) || lifecycle.has(message.to)));
143
+
144
+ const priorSession = ref.session;
130
145
  let revived = false;
131
- if (ref.status === "parked") {
146
+ if (needsLifecycleGate) {
132
147
  try {
133
- await this.#lifecycle().ensureLive(message.to);
134
- revived = true;
148
+ const liveSession = await lifecycle.ensureLive(message.to);
149
+ // Revival = we did not keep the same live instance (parked start, or
150
+ // park completed and a fresh session was rebuilt).
151
+ revived = !priorSession || liveSession !== priorSession;
135
152
  } catch (error) {
153
+ // Not revivable / released / revive failed. Do not buffer: a permanent
154
+ // failure must not inflate unread counts or pretend delivery is pending.
136
155
  return {
137
156
  to: message.to,
138
157
  outcome: "failed",
@@ -3,7 +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 { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
6
- import { isEexist, isEnoent, logger, postmortem, sanitizeText } from "@oh-my-pi/pi-utils";
6
+ import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
7
7
  import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
8
8
  import { workerEnvFromParent } from "../subprocess/worker-client";
9
9
  import { daemonBrokerEndpoint } from "./paths";
@@ -555,7 +555,8 @@ class DaemonBroker {
555
555
  `printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
556
556
  `exec ${argv.map(quoteShellArg).join(" ")}`,
557
557
  ].join("; ");
558
- run = session.start({ command, shell: process.env.SHELL, ...options }, onChunk);
558
+ const shell = procmgr.getShellConfig().shell;
559
+ run = session.start({ command, shell, ...options }, onChunk);
559
560
  }
560
561
  void run
561
562
  .then(result => this.#onPtyExit(record, generation, result))
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as net from "node:net";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { isEexist, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
5
+ import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
6
6
  import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
7
7
  import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
8
8
  import {
@@ -49,7 +49,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
49
49
  try {
50
50
  return await fs.realpath(resolved);
51
51
  } catch (error) {
52
- if (isEnoent(error)) return resolved;
52
+ if (isEnoent(error) || isEisdir(error)) return resolved;
53
53
  throw error;
54
54
  }
55
55
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { isEnoent, postmortem } from "@oh-my-pi/pi-utils";
3
+ import { isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { daemonRuntimeDir } from "./paths";
5
5
 
6
6
  const CLIENTS_DIR = "clients";
@@ -15,7 +15,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
15
15
  try {
16
16
  return await fs.realpath(resolved);
17
17
  } catch (error) {
18
- if (isEnoent(error)) return resolved;
18
+ if (isEnoent(error) || isEisdir(error)) return resolved;
19
19
  throw error;
20
20
  }
21
21
  }
package/src/lsp/client.ts CHANGED
@@ -142,6 +142,9 @@ const CLIENT_CAPABILITIES = {
142
142
  codeDescriptionSupport: true,
143
143
  dataSupport: true,
144
144
  },
145
+ diagnostic: {
146
+ dynamicRegistration: true,
147
+ },
145
148
  },
146
149
  window: {
147
150
  workDoneProgress: true,
@@ -428,7 +431,7 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
428
431
  const items = params?.items ?? [];
429
432
  const result = items.map(item => {
430
433
  const section = item.section ?? "";
431
- return client.config.settings?.[section] ?? {};
434
+ return client.config.settings?.[section] ?? null;
432
435
  });
433
436
  await sendResponse(client, message.id, result, "workspace/configuration");
434
437
  }
@@ -456,6 +459,58 @@ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequ
456
459
  }
457
460
  }
458
461
 
462
+ interface DynamicCapabilityRegistration {
463
+ id?: unknown;
464
+ method?: unknown;
465
+ }
466
+
467
+ interface DynamicCapabilityParams {
468
+ registrations?: DynamicCapabilityRegistration[];
469
+ unregisterations?: DynamicCapabilityRegistration[];
470
+ unregistrations?: DynamicCapabilityRegistration[];
471
+ }
472
+
473
+ function updateDynamicCapabilities(client: LspClient, message: LspJsonRpcRequest): void {
474
+ const params = message.params as DynamicCapabilityParams;
475
+ if (message.method === "client/registerCapability") {
476
+ if (!Array.isArray(params.registrations)) return;
477
+ let registrations = client.dynamicCapabilityRegistrations;
478
+ if (!registrations) {
479
+ registrations = new Map();
480
+ client.dynamicCapabilityRegistrations = registrations;
481
+ }
482
+ for (const registration of params.registrations) {
483
+ if (typeof registration.id === "string" && typeof registration.method === "string") {
484
+ registrations.set(registration.id, registration.method);
485
+ }
486
+ }
487
+ return;
488
+ }
489
+
490
+ const registrations = client.dynamicCapabilityRegistrations;
491
+ if (!registrations) return;
492
+ const unregistrations = params.unregisterations ?? params.unregistrations;
493
+ if (!Array.isArray(unregistrations)) return;
494
+ for (const registration of unregistrations) {
495
+ if (typeof registration.id === "string") {
496
+ registrations.delete(registration.id);
497
+ }
498
+ }
499
+ }
500
+
501
+ /** Whether the server advertised LSP 3.17 document diagnostic pulls statically or through registration. */
502
+ export function supportsDocumentDiagnostics(client: LspClient): boolean {
503
+ const staticProvider = client.serverCapabilities?.diagnosticProvider;
504
+ if (staticProvider) return true;
505
+
506
+ const registrations = client.dynamicCapabilityRegistrations;
507
+ if (!registrations) return false;
508
+ for (const method of registrations.values()) {
509
+ if (method === "textDocument/diagnostic") return true;
510
+ }
511
+ return false;
512
+ }
513
+
459
514
  /**
460
515
  * Respond to a server-initiated request.
461
516
  */
@@ -478,6 +533,7 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
478
533
  return;
479
534
  }
480
535
  if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
536
+ updateDynamicCapabilities(client, message);
481
537
  // Some servers block semantic requests until dynamic registration succeeds.
482
538
  await sendResponse(client, message.id, null, message.method);
483
539
  return;
@@ -685,6 +741,7 @@ export async function getOrCreateClient(
685
741
  requestId: 0,
686
742
  diagnostics: new Map(),
687
743
  diagnosticsVersion: 0,
744
+ dynamicCapabilityRegistrations: new Map(),
688
745
  openFiles: new Map(),
689
746
  pendingRequests: new Map(),
690
747
  messageBuffer: new Uint8Array(0),
package/src/lsp/index.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  sendNotification,
29
29
  sendRequest,
30
30
  setIdleTimeout,
31
+ supportsDocumentDiagnostics,
31
32
  syncContent,
32
33
  WARMUP_TIMEOUT_MS,
33
34
  waitForProjectLoaded,
@@ -530,17 +531,49 @@ interface WaitForDiagnosticsOptions {
530
531
  settleMs?: number;
531
532
  }
532
533
 
534
+ function requestDocumentDiagnostics(
535
+ client: LspClient,
536
+ uri: string,
537
+ signal: AbortSignal | undefined,
538
+ timeoutMs: number,
539
+ ): Promise<Diagnostic[] | undefined> {
540
+ return sendRequest(client, "textDocument/diagnostic", { textDocument: { uri } }, signal, timeoutMs)
541
+ .then(report => {
542
+ if (!report || typeof report !== "object" || !("kind" in report) || report.kind !== "full") {
543
+ return undefined;
544
+ }
545
+ if (!("items" in report) || !Array.isArray(report.items)) return undefined;
546
+ return report.items;
547
+ })
548
+ .catch(err => {
549
+ if (!signal?.aborted) {
550
+ logger.debug("LSP document diagnostic pull failed", { server: client.name, uri, error: String(err) });
551
+ }
552
+ return undefined;
553
+ });
554
+ }
555
+
533
556
  async function waitForDiagnostics(
534
557
  client: LspClient,
535
558
  uri: string,
536
559
  options: WaitForDiagnosticsOptions = {},
537
560
  ): Promise<Diagnostic[]> {
538
561
  const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
539
- const start = Date.now();
562
+ const deadline = Date.now() + timeoutMs;
563
+ let pullAttempted = false;
564
+ let pullResultPromise: Promise<{ diagnostics: Diagnostic[] | undefined }> | undefined;
565
+ let pulled: Diagnostic[] | undefined;
540
566
  let settledRef: PublishedDiagnostics | undefined;
541
567
  let settledAt = 0;
542
- while (Date.now() - start < timeoutMs) {
568
+ while (Date.now() < deadline) {
543
569
  throwIfAborted(signal);
570
+ if (!pullAttempted && supportsDocumentDiagnostics(client)) {
571
+ pullAttempted = true;
572
+ pullResultPromise = requestDocumentDiagnostics(client, uri, signal, Math.max(1, deadline - Date.now())).then(
573
+ diagnostics => ({ diagnostics }),
574
+ );
575
+ }
576
+
544
577
  const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
545
578
  const published = client.diagnostics.get(uri);
546
579
  if (published && versionOk) {
@@ -557,13 +590,36 @@ async function waitForDiagnostics(
557
590
  return published.diagnostics;
558
591
  }
559
592
  }
560
- await Bun.sleep(DIAGNOSTICS_POLL_MS);
593
+
594
+ const pollMs = Math.min(DIAGNOSTICS_POLL_MS, Math.max(0, deadline - Date.now()));
595
+ if (!pullResultPromise) {
596
+ await Bun.sleep(pollMs);
597
+ continue;
598
+ }
599
+ const pullResult = await Promise.race([pullResultPromise, Bun.sleep(pollMs).then(() => undefined)]);
600
+ if (pullResult) {
601
+ pullResultPromise = undefined;
602
+ pulled = pullResult.diagnostics;
603
+ if (pulled !== undefined) break;
604
+ }
561
605
  }
606
+
562
607
  const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
563
- if (!versionOk) {
564
- return [];
608
+ const published = client.diagnostics.get(uri);
609
+ if (published && versionOk) {
610
+ return published.diagnostics;
611
+ }
612
+ if (pullResultPromise) {
613
+ pulled = (await pullResultPromise).diagnostics;
565
614
  }
566
- return client.diagnostics.get(uri)?.diagnostics ?? [];
615
+ throwIfAborted(signal);
616
+ if (pulled === undefined) return [];
617
+ client.diagnostics.set(uri, {
618
+ diagnostics: pulled,
619
+ version: expectedDocumentVersion ?? client.openFiles.get(uri)?.version ?? null,
620
+ });
621
+ client.diagnosticsVersion += 1;
622
+ return pulled;
567
623
  }
568
624
 
569
625
  /** Project type detection result */
package/src/lsp/types.ts CHANGED
@@ -387,6 +387,7 @@ export interface LspServerCapabilities {
387
387
  referencesProvider?: boolean;
388
388
  documentSymbolProvider?: boolean;
389
389
  workspaceSymbolProvider?: boolean;
390
+ diagnosticProvider?: boolean | Record<string, unknown>;
390
391
  [key: string]: unknown;
391
392
  }
392
393
 
@@ -398,6 +399,8 @@ export interface LspClient {
398
399
  requestId: number;
399
400
  diagnostics: Map<string, PublishedDiagnostics>;
400
401
  diagnosticsVersion: number;
402
+ /** Dynamic capability registrations keyed by the server-provided registration ID. */
403
+ dynamicCapabilityRegistrations?: Map<string, string>;
401
404
  openFiles: Map<string, OpenFile>;
402
405
  pendingRequests: Map<number | string, PendingRequest>;
403
406
  messageBuffer: Uint8Array;
package/src/main.ts CHANGED
@@ -59,6 +59,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
59
59
  import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
60
60
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
61
61
  import type { SubmittedUserInput } from "./modes/types";
62
+ import { createWarpEventBridgeExtension } from "./modes/warp-events";
62
63
  import { AgentLifecycleManager } from "./registry/agent-lifecycle";
63
64
  import {
64
65
  type CreateAgentSessionOptions,
@@ -76,7 +77,7 @@ import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
76
77
  import { shouldShowStartupSplash } from "./startup-splash";
77
78
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
78
79
  import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
79
- import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
80
+ import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
80
81
  import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
81
82
  import type { LspStartupServerInfo } from "./tools";
82
83
  import {
@@ -1350,15 +1351,13 @@ export async function runRootCommand(
1350
1351
  sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
1351
1352
  sessionOptions.settings = settingsInstance;
1352
1353
 
1353
- // OTEL: register the global OTLP trace exporter when an OTLP endpoint is
1354
- // configured via env, then switch on the agent loop's telemetry so its
1355
- // GenAI spans (invoke_agent / chat / execute_tool) are actually emitted.
1356
- // Both are no-ops when OTEL_EXPORTER_OTLP_ENDPOINT is unset. An empty config
1357
- // is enough to enable telemetry — content capture is governed by the
1358
- // standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
1354
+ // OTEL: register global OTLP exporters when an endpoint is configured via
1355
+ // env, then switch on the agent loop's telemetry hooks so traces, run-level
1356
+ // metrics, and structured logs have source events to export. Content capture
1357
+ // remains governed by OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.
1359
1358
  await logger.time("initTelemetryExport", initTelemetryExport);
1360
1359
  if (isTelemetryExportEnabled()) {
1361
- sessionOptions.telemetry = {};
1360
+ sessionOptions.telemetry = createTelemetryExportConfig(sessionOptions.telemetry);
1362
1361
  }
1363
1362
 
1364
1363
  // Handle CLI --api-key as runtime override (not persisted)
@@ -1407,6 +1406,10 @@ export async function runRootCommand(
1407
1406
  // string-flag value such as `--target @notes.md` is the flag's value, not a
1408
1407
  // file — and the same result is handed to createAgentSession via
1409
1408
  // `preloadedExtensions` so the discovery work is not repeated.
1409
+ if (isInteractive) {
1410
+ sessionOptions.extensions = [...(sessionOptions.extensions ?? []), createWarpEventBridgeExtension()];
1411
+ }
1412
+
1410
1413
  const eventBus = new EventBus();
1411
1414
  const extensionsResult = await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
1412
1415
  const extensionFlagSink: ExtensionFlagSink = {
@@ -572,8 +572,14 @@ export class MCPManager {
572
572
  };
573
573
  }
574
574
 
575
+ /**
576
+ * Ownership is matched via `mcpServerName`, never a `mcp__${name}_` name
577
+ * prefix: tool names are lossy-sanitized, so one server's sanitized name
578
+ * can prefix another's (`atlassian` vs `atlassian:atlassian`) and a name
579
+ * with sanitized characters never prefix-matches its own tools at all.
580
+ */
575
581
  #replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
576
- this.#tools = this.#tools.filter(t => !t.name.startsWith(`mcp__${name}_`));
582
+ this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
577
583
  this.#tools.push(...tools);
578
584
  // Stable sort by name so reconnect order does not perturb the array.
579
585
  // See `sortMCPToolsByName` for the cache-stability rationale.
@@ -762,8 +768,8 @@ export class MCPManager {
762
768
  }
763
769
 
764
770
  // Remove tools from this server and notify consumers
765
- const hadTools = this.#tools.some(t => t.name.startsWith(`mcp__${name}_`));
766
- this.#tools = this.#tools.filter(t => !t.name.startsWith(`mcp__${name}_`));
771
+ const hadTools = this.#tools.some(t => t.mcpServerName === name);
772
+ this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
767
773
  if (hadTools) this.#onToolsChanged?.(this.#tools);
768
774
 
769
775
  // Notify prompt consumers so stale commands are cleared
@@ -385,6 +385,13 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
385
385
  async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
386
386
  if (!this.#resolvedClientId) {
387
387
  await this.#tryRegisterClient(redirectUri);
388
+ // `unapproved_client` explicitly establishes that registration cannot
389
+ // produce the required client id. Other DCR failures stay on the
390
+ // clientless probe path because they may be transient or caused by
391
+ // unrelated registration metadata.
392
+ if (!this.#resolvedClientId && this.#isDefinitiveRegistrationRejection()) {
393
+ throw this.#missingClientIdError();
394
+ }
388
395
  }
389
396
 
390
397
  const authUrl = new URL(this.config.authorizationUrl);
@@ -691,6 +698,19 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
691
698
  }
692
699
  }
693
700
 
701
+ /**
702
+ * Whether the provider explicitly rejected this client as unapproved.
703
+ *
704
+ * HTTP status alone is insufficient: payload errors such as
705
+ * `invalid_client_metadata` and `invalid_redirect_uri` do not establish that
706
+ * the authorization endpoint requires a client id. Keep those on the
707
+ * clientless probe path.
708
+ */
709
+ #isDefinitiveRegistrationRejection(): boolean {
710
+ const failure = this.#registrationFailure;
711
+ return failure?.status === 403 && /\bunapproved_client\b/i.test(failure.detail ?? "");
712
+ }
713
+
694
714
  /**
695
715
  * Build the error thrown when the authorize probe confirms the provider
696
716
  * demands a `client_id`. When dynamic client registration was attempted and