@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2

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 (165) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +10 -10
  4. package/scripts/build-binary.ts +5 -0
  5. package/src/autoresearch/helpers.ts +17 -0
  6. package/src/autoresearch/tools/log-experiment.ts +9 -17
  7. package/src/autoresearch/tools/run-experiment.ts +2 -17
  8. package/src/capability/skill.ts +7 -0
  9. package/src/cli/list-models.ts +1 -1
  10. package/src/cli/shell-cli.ts +3 -13
  11. package/src/cli/update-cli.ts +1 -1
  12. package/src/cli.ts +10 -29
  13. package/src/commands/commit.ts +10 -0
  14. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  15. package/src/commit/analysis/conventional.ts +8 -66
  16. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  17. package/src/commit/pipeline.ts +2 -2
  18. package/src/commit/shared-llm.ts +89 -0
  19. package/src/config/config-file.ts +210 -0
  20. package/src/config/model-equivalence.ts +8 -11
  21. package/src/config/model-registry.ts +44 -3
  22. package/src/config/model-resolver.ts +1 -4
  23. package/src/config/settings-schema.ts +82 -1
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -219
  26. package/src/discovery/claude-plugins.ts +19 -7
  27. package/src/edit/renderer.ts +7 -1
  28. package/src/eval/js/executor.ts +3 -0
  29. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  30. package/src/eval/py/executor.ts +5 -0
  31. package/src/eval/py/runner.py +42 -11
  32. package/src/eval/py/runtime.ts +1 -0
  33. package/src/exa/factory.ts +2 -2
  34. package/src/exa/mcp-client.ts +74 -1
  35. package/src/exec/bash-executor.ts +5 -1
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +0 -11
  38. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  39. package/src/extensibility/extensions/runner.ts +1 -1
  40. package/src/extensibility/extensions/types.ts +89 -223
  41. package/src/extensibility/hooks/types.ts +89 -314
  42. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  43. package/src/extensibility/shared-events.ts +343 -0
  44. package/src/extensibility/skills.ts +9 -0
  45. package/src/goals/index.ts +3 -0
  46. package/src/goals/runtime.ts +500 -0
  47. package/src/goals/state.ts +37 -0
  48. package/src/goals/tools/goal-tool.ts +237 -0
  49. package/src/hashline/anchors.ts +2 -2
  50. package/src/hashline/input.ts +2 -1
  51. package/src/hashline/parser.ts +27 -3
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/agent-protocol.ts +1 -20
  54. package/src/internal-urls/artifact-protocol.ts +1 -19
  55. package/src/internal-urls/docs-index.generated.ts +11 -12
  56. package/src/internal-urls/registry-helpers.ts +25 -0
  57. package/src/internal-urls/router.ts +8 -0
  58. package/src/internal-urls/types.ts +21 -0
  59. package/src/lsp/config.ts +15 -6
  60. package/src/lsp/defaults.json +6 -2
  61. package/src/main.ts +11 -2
  62. package/src/mcp/oauth-flow.ts +20 -0
  63. package/src/modes/acp/acp-agent.ts +327 -95
  64. package/src/modes/components/assistant-message.ts +14 -8
  65. package/src/modes/components/bash-execution.ts +24 -63
  66. package/src/modes/components/custom-message.ts +14 -40
  67. package/src/modes/components/eval-execution.ts +27 -57
  68. package/src/modes/components/execution-shared.ts +102 -0
  69. package/src/modes/components/hook-message.ts +17 -49
  70. package/src/modes/components/mcp-add-wizard.ts +26 -5
  71. package/src/modes/components/message-frame.ts +88 -0
  72. package/src/modes/components/model-selector.ts +1 -1
  73. package/src/modes/components/session-observer-overlay.ts +6 -2
  74. package/src/modes/components/session-selector.ts +1 -1
  75. package/src/modes/components/status-line/segments.ts +93 -8
  76. package/src/modes/components/status-line/types.ts +4 -0
  77. package/src/modes/components/status-line.ts +28 -10
  78. package/src/modes/components/tool-execution.ts +7 -8
  79. package/src/modes/controllers/command-controller-shared.ts +108 -0
  80. package/src/modes/controllers/command-controller.ts +13 -4
  81. package/src/modes/controllers/event-controller.ts +36 -7
  82. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  83. package/src/modes/controllers/input-controller.ts +13 -0
  84. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  85. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  86. package/src/modes/interactive-mode.ts +624 -52
  87. package/src/modes/print-mode.ts +16 -86
  88. package/src/modes/rpc/host-uris.ts +235 -0
  89. package/src/modes/rpc/rpc-mode.ts +41 -88
  90. package/src/modes/rpc/rpc-types.ts +57 -0
  91. package/src/modes/runtime-init.ts +116 -0
  92. package/src/modes/theme/defaults/dark-poimandres.json +3 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +3 -0
  94. package/src/modes/theme/theme.ts +24 -6
  95. package/src/modes/types.ts +14 -3
  96. package/src/modes/utils/context-usage.ts +13 -13
  97. package/src/modes/utils/ui-helpers.ts +10 -3
  98. package/src/plan-mode/approved-plan.ts +35 -1
  99. package/src/prompts/goals/goal-budget-limit.md +16 -0
  100. package/src/prompts/goals/goal-continuation.md +28 -0
  101. package/src/prompts/goals/goal-mode-active.md +23 -0
  102. package/src/prompts/system/plan-mode-active.md +5 -5
  103. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  104. package/src/prompts/tools/bash.md +6 -0
  105. package/src/prompts/tools/github.md +4 -4
  106. package/src/prompts/tools/goal.md +13 -0
  107. package/src/prompts/tools/hashline.md +101 -117
  108. package/src/prompts/tools/read.md +55 -36
  109. package/src/prompts/tools/resolve.md +6 -5
  110. package/src/sdk.ts +12 -5
  111. package/src/session/agent-session.ts +428 -106
  112. package/src/session/blob-store.ts +36 -3
  113. package/src/session/messages.ts +67 -2
  114. package/src/session/session-manager.ts +131 -12
  115. package/src/session/session-storage.ts +33 -15
  116. package/src/session/streaming-output.ts +309 -13
  117. package/src/slash-commands/builtin-registry.ts +18 -0
  118. package/src/ssh/ssh-executor.ts +5 -0
  119. package/src/system-prompt.ts +4 -2
  120. package/src/task/discovery.ts +5 -2
  121. package/src/task/executor.ts +19 -8
  122. package/src/task/index.ts +3 -0
  123. package/src/task/render.ts +21 -15
  124. package/src/task/types.ts +4 -0
  125. package/src/tools/ast-edit.ts +21 -120
  126. package/src/tools/ast-grep.ts +21 -119
  127. package/src/tools/bash-command-fixup.ts +47 -0
  128. package/src/tools/bash-interactive.ts +9 -1
  129. package/src/tools/bash.ts +66 -19
  130. package/src/tools/browser/attach.ts +3 -3
  131. package/src/tools/browser/launch.ts +81 -18
  132. package/src/tools/browser/registry.ts +1 -5
  133. package/src/tools/browser/render.ts +2 -2
  134. package/src/tools/browser/tab-supervisor.ts +51 -14
  135. package/src/tools/conflict-detect.ts +15 -4
  136. package/src/tools/eval.ts +12 -2
  137. package/src/tools/find.ts +20 -38
  138. package/src/tools/gh.ts +44 -10
  139. package/src/tools/index.ts +22 -11
  140. package/src/tools/inspect-image.ts +3 -10
  141. package/src/tools/job.ts +16 -7
  142. package/src/tools/output-meta.ts +202 -37
  143. package/src/tools/path-utils.ts +125 -2
  144. package/src/tools/read.ts +548 -237
  145. package/src/tools/render-utils.ts +92 -0
  146. package/src/tools/renderers.ts +2 -0
  147. package/src/tools/resolve.ts +72 -44
  148. package/src/tools/search.ts +120 -186
  149. package/src/tools/ssh.ts +3 -2
  150. package/src/tools/write.ts +64 -9
  151. package/src/utils/file-mentions.ts +1 -1
  152. package/src/utils/image-loading.ts +7 -3
  153. package/src/utils/image-resize.ts +32 -43
  154. package/src/vim/parser.ts +0 -17
  155. package/src/vim/render.ts +1 -1
  156. package/src/vim/types.ts +1 -1
  157. package/src/web/search/providers/anthropic.ts +5 -0
  158. package/src/web/search/providers/exa.ts +3 -0
  159. package/src/web/search/providers/gemini.ts +40 -95
  160. package/src/web/search/providers/jina.ts +5 -2
  161. package/src/web/search/providers/zai.ts +5 -2
  162. package/src/prompts/tools/exit-plan-mode.md +0 -6
  163. package/src/tools/exit-plan-mode.ts +0 -97
  164. package/src/utils/fuzzy.ts +0 -108
  165. package/src/utils/image-convert.ts +0 -27
@@ -7,8 +7,9 @@
7
7
  */
8
8
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import { sanitizeText } from "@oh-my-pi/pi-natives";
10
- import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
11
10
  import type { AgentSession } from "../session/agent-session";
11
+ import { isSilentAbort } from "../session/messages";
12
+ import { initializeExtensions } from "./runtime-init";
12
13
 
13
14
  /**
14
15
  * Options for print mode.
@@ -39,90 +40,16 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
39
40
  }
40
41
  }
41
42
  // Set up extensions for print mode (no UI, no command context)
42
- const extensionRunner = session.extensionRunner;
43
- if (extensionRunner) {
44
- extensionRunner.initialize(
45
- // ExtensionActions
46
- {
47
- sendMessage: (message, options) => {
48
- session.sendCustomMessage(message, options).catch(e => {
49
- process.stderr.write(`Extension sendMessage failed: ${e instanceof Error ? e.message : String(e)}\n`);
50
- });
51
- },
52
- sendUserMessage: (content, options) => {
53
- session.sendUserMessage(content, options).catch(e => {
54
- process.stderr.write(
55
- `Extension sendUserMessage failed: ${e instanceof Error ? e.message : String(e)}\n`,
56
- );
57
- });
58
- },
59
- appendEntry: (customType, data) => {
60
- session.sessionManager.appendCustomEntry(customType, data);
61
- },
62
- setLabel: (targetId, label) => {
63
- session.sessionManager.appendLabelChange(targetId, label);
64
- },
65
- getActiveTools: () => session.getActiveToolNames(),
66
- getAllTools: () => session.getAllToolNames(),
67
- setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
68
- getCommands: () => [],
69
- setModel: model => runExtensionSetModel(session, model),
70
- getThinkingLevel: () => session.thinkingLevel,
71
- setThinkingLevel: level => session.setThinkingLevel(level),
72
- getSessionName: () => session.sessionManager.getSessionName(),
73
- setSessionName: async name => {
74
- await session.sessionManager.setSessionName(name, "user");
75
- },
76
- },
77
- // ExtensionContextActions
78
- {
79
- getModel: () => session.model,
80
- isIdle: () => !session.isStreaming,
81
- abort: () => session.abort(),
82
- hasPendingMessages: () => session.queuedMessageCount > 0,
83
- shutdown: () => {},
84
- getContextUsage: () => session.getContextUsage(),
85
- getSystemPrompt: () => session.systemPrompt,
86
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
87
- },
88
- // ExtensionCommandContextActions - commands invokable via prompt("/command")
89
- {
90
- getContextUsage: () => session.getContextUsage(),
91
- waitForIdle: () => session.agent.waitForIdle(),
92
- newSession: async options => {
93
- const success = await session.newSession({ parentSession: options?.parentSession });
94
- if (success && options?.setup) {
95
- await options.setup(session.sessionManager);
96
- }
97
- return { cancelled: !success };
98
- },
99
- branch: async entryId => {
100
- const result = await session.branch(entryId);
101
- return { cancelled: result.cancelled };
102
- },
103
- navigateTree: async (targetId, options) => {
104
- const result = await session.navigateTree(targetId, { summarize: options?.summarize });
105
- return { cancelled: result.cancelled };
106
- },
107
- switchSession: async sessionPath => {
108
- const success = await session.switchSession(sessionPath);
109
- return { cancelled: !success };
110
- },
111
- reload: async () => {
112
- await session.reload();
113
- },
114
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
115
- },
116
- // No UI context
117
- );
118
- extensionRunner.onError(err => {
43
+ await initializeExtensions(session, {
44
+ reportSendError: (action, err) => {
45
+ process.stderr.write(
46
+ `Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
47
+ );
48
+ },
49
+ reportRuntimeError: err => {
119
50
  process.stderr.write(`Extension error (${err.extensionPath}): ${err.error}\n`);
120
- });
121
- // Emit session_start event
122
- await extensionRunner.emit({
123
- type: "session_start",
124
- });
125
- }
51
+ },
52
+ });
126
53
 
127
54
  // Always subscribe to enable session persistence via _handleAgentEvent
128
55
  session.subscribe(event => {
@@ -150,8 +77,11 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
150
77
  if (lastMessage?.role === "assistant") {
151
78
  const assistantMsg = lastMessage as AssistantMessage;
152
79
 
153
- // Check for error/aborted
154
- if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
80
+ // Check for error/aborted — skip silent-abort (plan-mode compaction transition)
81
+ if (
82
+ (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
83
+ !isSilentAbort(assistantMsg.errorMessage)
84
+ ) {
155
85
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
156
86
  const flushed = process.stderr.write(`${errorLine}\n`);
157
87
  if (flushed) {
@@ -0,0 +1,235 @@
1
+ import { Snowflake } from "@oh-my-pi/pi-utils";
2
+ import { InternalUrlRouter } from "../../internal-urls";
3
+ import type {
4
+ InternalResource,
5
+ InternalUrl,
6
+ ProtocolHandler,
7
+ ResolveContext,
8
+ WriteContext,
9
+ } from "../../internal-urls/types";
10
+ import type {
11
+ RpcHostUriCancelRequest,
12
+ RpcHostUriRequest,
13
+ RpcHostUriResult,
14
+ RpcHostUriSchemeDefinition,
15
+ } from "./rpc-types";
16
+
17
+ type RpcHostUriOutput = (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
18
+
19
+ type PendingUriRequest = {
20
+ operation: "read" | "write";
21
+ url: string;
22
+ resolve: (frame: RpcHostUriResult) => void;
23
+ reject: (error: Error) => void;
24
+ };
25
+
26
+ /** Type guard for inbound `host_uri_result` frames coming from the host. */
27
+ export function isRpcHostUriResult(value: unknown): value is RpcHostUriResult {
28
+ if (!value || typeof value !== "object") return false;
29
+ const frame = value as { type?: unknown; id?: unknown };
30
+ return frame.type === "host_uri_result" && typeof frame.id === "string";
31
+ }
32
+
33
+ /**
34
+ * One handler instance per host-registered scheme. Delegates reads and (when
35
+ * the scheme was registered as writable) writes to the bridge, which serializes
36
+ * them over the RPC transport.
37
+ */
38
+ class RpcHostUriProtocolHandler implements ProtocolHandler {
39
+ readonly scheme: string;
40
+ readonly immutable: boolean;
41
+ readonly write?: (url: InternalUrl, content: string, context?: WriteContext) => Promise<void>;
42
+ readonly #bridge: RpcHostUriBridge;
43
+
44
+ constructor(definition: RpcHostUriSchemeDefinition, bridge: RpcHostUriBridge) {
45
+ this.scheme = definition.scheme;
46
+ this.immutable = definition.immutable === true;
47
+ this.#bridge = bridge;
48
+ if (definition.writable === true) {
49
+ this.write = (url, content, context) => this.#bridge.requestWrite(this.scheme, url, content, context);
50
+ }
51
+ }
52
+
53
+ resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
54
+ return this.#bridge.requestRead(this.scheme, url, context);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Bidirectional bridge that lets the RPC host own a set of URI schemes.
60
+ *
61
+ * The host registers schemes via `set_host_uri_schemes`; the bridge installs
62
+ * a `RpcHostUriProtocolHandler` per scheme into the process-global
63
+ * {@link InternalUrlRouter}. Reads land on the read tool through the existing
64
+ * router; writes are intercepted by the write tool and dispatched through
65
+ * `requestWrite`.
66
+ */
67
+ export class RpcHostUriBridge {
68
+ #output: RpcHostUriOutput;
69
+ #router: InternalUrlRouter;
70
+ #definitions = new Map<string, RpcHostUriSchemeDefinition>();
71
+ #pending = new Map<string, PendingUriRequest>();
72
+
73
+ constructor(output: RpcHostUriOutput, router: InternalUrlRouter = InternalUrlRouter.instance()) {
74
+ this.#output = output;
75
+ this.#router = router;
76
+ }
77
+
78
+ getSchemes(): string[] {
79
+ return Array.from(this.#definitions.keys());
80
+ }
81
+
82
+ /**
83
+ * Replace the registered set of host URI schemes. Previously registered
84
+ * schemes that no longer appear in the new set are unregistered from the
85
+ * router; surviving and new schemes get fresh handler instances.
86
+ */
87
+ setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[] {
88
+ const normalized = new Map<string, RpcHostUriSchemeDefinition>();
89
+ for (const raw of schemes) {
90
+ const scheme = typeof raw?.scheme === "string" ? raw.scheme.trim().toLowerCase() : "";
91
+ if (!scheme) {
92
+ throw new Error("Host URI scheme must be a non-empty string");
93
+ }
94
+ if (!/^[a-z][a-z0-9+.-]*$/.test(scheme)) {
95
+ throw new Error(`Host URI scheme contains invalid characters: ${raw.scheme}`);
96
+ }
97
+ normalized.set(scheme, {
98
+ scheme,
99
+ description: typeof raw.description === "string" ? raw.description : undefined,
100
+ writable: raw.writable === true,
101
+ immutable: raw.immutable === true,
102
+ });
103
+ }
104
+
105
+ for (const previous of this.#definitions.keys()) {
106
+ if (!normalized.has(previous)) {
107
+ this.#router.unregister(previous);
108
+ }
109
+ }
110
+ for (const definition of normalized.values()) {
111
+ this.#router.register(new RpcHostUriProtocolHandler(definition, this));
112
+ }
113
+ this.#definitions = normalized;
114
+ return Array.from(normalized.keys());
115
+ }
116
+
117
+ /**
118
+ * Unregister every host scheme from the router and reject any in-flight
119
+ * requests. Called on RPC shutdown to keep the global router clean for
120
+ * subsequent sessions in the same process (used by tests).
121
+ */
122
+ clear(message: string = "Host URI bridge shut down"): void {
123
+ for (const scheme of this.#definitions.keys()) {
124
+ this.#router.unregister(scheme);
125
+ }
126
+ this.#definitions.clear();
127
+ this.rejectAllPending(message);
128
+ }
129
+
130
+ /** Resolve a pending request by id; called by `rpc-mode` on inbound results. */
131
+ handleResult(frame: RpcHostUriResult): boolean {
132
+ const pending = this.#pending.get(frame.id);
133
+ if (!pending) return false;
134
+ this.#pending.delete(frame.id);
135
+ pending.resolve(frame);
136
+ return true;
137
+ }
138
+
139
+ rejectAllPending(message: string): void {
140
+ const error = new Error(message);
141
+ const pending = Array.from(this.#pending.values());
142
+ this.#pending.clear();
143
+ for (const entry of pending) {
144
+ entry.reject(error);
145
+ }
146
+ }
147
+
148
+ async requestRead(scheme: string, url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
149
+ const result = await this.#dispatch("read", url.href, undefined, context?.signal);
150
+ if (result.isError) {
151
+ throw new Error(result.error || result.content || `Host URI read failed for ${url.href}`);
152
+ }
153
+ const content = result.content ?? "";
154
+ const contentType = result.contentType ?? "text/plain";
155
+ const definition = this.#definitions.get(scheme);
156
+ return {
157
+ url: url.href,
158
+ content,
159
+ contentType,
160
+ size: Buffer.byteLength(content, "utf-8"),
161
+ notes: result.notes && result.notes.length > 0 ? [...result.notes] : undefined,
162
+ immutable: result.immutable ?? definition?.immutable === true,
163
+ };
164
+ }
165
+
166
+ async requestWrite(_scheme: string, url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
167
+ const result = await this.#dispatch("write", url.href, content, context?.signal);
168
+ if (result.isError) {
169
+ throw new Error(result.error || result.content || `Host URI write failed for ${url.href}`);
170
+ }
171
+ }
172
+
173
+ #dispatch(
174
+ operation: "read" | "write",
175
+ url: string,
176
+ content: string | undefined,
177
+ signal: AbortSignal | undefined,
178
+ ): Promise<RpcHostUriResult> {
179
+ if (signal?.aborted) {
180
+ return Promise.reject(new Error(`Host URI ${operation} for ${url} was aborted`));
181
+ }
182
+
183
+ const id = Snowflake.next() as string;
184
+ const { promise, resolve, reject } = Promise.withResolvers<RpcHostUriResult>();
185
+ let settled = false;
186
+
187
+ const cleanup = () => {
188
+ signal?.removeEventListener("abort", onAbort);
189
+ this.#pending.delete(id);
190
+ };
191
+
192
+ const onAbort = () => {
193
+ if (settled) return;
194
+ settled = true;
195
+ cleanup();
196
+ this.#output({
197
+ type: "host_uri_cancel",
198
+ id: Snowflake.next() as string,
199
+ targetId: id,
200
+ });
201
+ reject(new Error(`Host URI ${operation} for ${url} was aborted`));
202
+ };
203
+
204
+ signal?.addEventListener("abort", onAbort, { once: true });
205
+ this.#pending.set(id, {
206
+ operation,
207
+ url,
208
+ resolve: frame => {
209
+ if (settled) return;
210
+ settled = true;
211
+ cleanup();
212
+ resolve(frame);
213
+ },
214
+ reject: err => {
215
+ if (settled) return;
216
+ settled = true;
217
+ cleanup();
218
+ reject(err);
219
+ },
220
+ });
221
+
222
+ const frame: RpcHostUriRequest = {
223
+ type: "host_uri_request",
224
+ id,
225
+ operation,
226
+ url,
227
+ };
228
+ if (operation === "write") {
229
+ frame.content = content ?? "";
230
+ }
231
+ this.#output(frame);
232
+
233
+ return promise;
234
+ }
235
+ }
@@ -17,10 +17,11 @@ import type {
17
17
  ExtensionUIDialogOptions,
18
18
  ExtensionWidgetOptions,
19
19
  } from "../../extensibility/extensions";
20
- import { runExtensionCompact, runExtensionSetModel } from "../../extensibility/extensions/compact-handler";
21
20
  import { type Theme, theme } from "../../modes/theme/theme";
22
21
  import type { AgentSession } from "../../session/agent-session";
22
+ import { initializeExtensions } from "../runtime-init";
23
23
  import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
24
+ import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
24
25
  import type {
25
26
  RpcCommand,
26
27
  RpcExtensionUIRequest,
@@ -28,6 +29,8 @@ import type {
28
29
  RpcHostToolCallRequest,
29
30
  RpcHostToolCancelRequest,
30
31
  RpcHostToolDefinition,
32
+ RpcHostUriCancelRequest,
33
+ RpcHostUriRequest,
31
34
  RpcResponse,
32
35
  RpcSessionState,
33
36
  } from "./rpc-types";
@@ -41,7 +44,14 @@ export type PendingExtensionRequest = {
41
44
  };
42
45
 
43
46
  type RpcOutput = (
44
- obj: RpcResponse | RpcExtensionUIRequest | RpcHostToolCallRequest | RpcHostToolCancelRequest | object,
47
+ obj:
48
+ | RpcResponse
49
+ | RpcExtensionUIRequest
50
+ | RpcHostToolCallRequest
51
+ | RpcHostToolCancelRequest
52
+ | RpcHostUriRequest
53
+ | RpcHostUriCancelRequest
54
+ | object,
45
55
  ) => void;
46
56
 
47
57
  function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
@@ -188,6 +198,7 @@ export async function runRpcMode(
188
198
 
189
199
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
190
200
  const hostToolBridge = new RpcHostToolBridge(output);
201
+ const hostUriBridge = new RpcHostUriBridge(output);
191
202
 
192
203
  // Shutdown request flag (wrapped in object to allow mutation with const)
193
204
  const shutdownState = { requested: false };
@@ -421,91 +432,18 @@ export async function runRpcMode(
421
432
  setToolUIContext?.(rpcUiContext, true);
422
433
 
423
434
  // Set up extensions with RPC-based UI context
424
- const extensionRunner = session.extensionRunner;
425
- if (extensionRunner) {
426
- extensionRunner.initialize(
427
- // ExtensionActions
428
- {
429
- sendMessage: (message, options) => {
430
- session.sendCustomMessage(message, options).catch(e => {
431
- output(error(undefined, "extension_send", e.message));
432
- });
433
- },
434
- sendUserMessage: (content, options) => {
435
- session.sendUserMessage(content, options).catch(e => {
436
- output(error(undefined, "extension_send_user", e.message));
437
- });
438
- },
439
- appendEntry: (customType, data) => {
440
- session.sessionManager.appendCustomEntry(customType, data);
441
- },
442
- setLabel: (targetId, label) => {
443
- session.sessionManager.appendLabelChange(targetId, label);
444
- },
445
- getActiveTools: () => session.getActiveToolNames(),
446
- getAllTools: () => session.getAllToolNames(),
447
- setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
448
- getCommands: () => [],
449
- setModel: model => runExtensionSetModel(session, model),
450
- getThinkingLevel: () => session.thinkingLevel,
451
- setThinkingLevel: level => session.setThinkingLevel(level),
452
- getSessionName: () => session.sessionManager.getSessionName(),
453
- setSessionName: async name => {
454
- await session.sessionManager.setSessionName(name, "user");
455
- },
456
- },
457
- // ExtensionContextActions
458
- {
459
- getModel: () => session.agent.state.model,
460
- isIdle: () => !session.isStreaming,
461
- abort: () => session.abort(),
462
- hasPendingMessages: () => session.queuedMessageCount > 0,
463
- shutdown: () => {
464
- shutdownState.requested = true;
465
- },
466
- getContextUsage: () => session.getContextUsage(),
467
- getSystemPrompt: () => session.systemPrompt,
468
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
469
- },
470
- // ExtensionCommandContextActions - commands invokable via prompt("/command")
471
- {
472
- getContextUsage: () => session.getContextUsage(),
473
- waitForIdle: () => session.agent.waitForIdle(),
474
- newSession: async options => {
475
- const success = await session.newSession({ parentSession: options?.parentSession });
476
- // Note: setup callback runs but no UI feedback in RPC mode
477
- if (success && options?.setup) {
478
- await options.setup(session.sessionManager);
479
- }
480
- return { cancelled: !success };
481
- },
482
- branch: async entryId => {
483
- const result = await session.branch(entryId);
484
- return { cancelled: result.cancelled };
485
- },
486
- navigateTree: async (targetId, options) => {
487
- const result = await session.navigateTree(targetId, { summarize: options?.summarize });
488
- return { cancelled: result.cancelled };
489
- },
490
- switchSession: async sessionPath => {
491
- const success = await session.switchSession(sessionPath);
492
- return { cancelled: !success };
493
- },
494
- reload: async () => {
495
- await session.reload();
496
- },
497
- compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
498
- },
499
- rpcUiContext,
500
- );
501
- extensionRunner.onError(err => {
435
+ await initializeExtensions(session, {
436
+ reportSendError: (action, err) => {
437
+ output(error(undefined, action, err.message));
438
+ },
439
+ reportRuntimeError: err => {
502
440
  output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
503
- });
504
- // Emit session_start event
505
- await extensionRunner.emit({
506
- type: "session_start",
507
- });
508
- }
441
+ },
442
+ onShutdown: () => {
443
+ shutdownState.requested = true;
444
+ },
445
+ uiContext: rpcUiContext,
446
+ });
509
447
 
510
448
  // Output all agent events as JSON
511
449
  session.subscribe(event => {
@@ -606,6 +544,15 @@ export async function runRpcMode(
606
544
  return success(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
607
545
  }
608
546
 
547
+ case "set_host_uri_schemes": {
548
+ try {
549
+ const schemes = hostUriBridge.setSchemes(command.schemes);
550
+ return success(id, "set_host_uri_schemes", { schemes });
551
+ } catch (err) {
552
+ return error(id, "set_host_uri_schemes", err instanceof Error ? err.message : String(err));
553
+ }
554
+ }
555
+
609
556
  // =================================================================
610
557
  // Model
611
558
  // =================================================================
@@ -850,8 +797,8 @@ export async function runRpcMode(
850
797
  async function checkShutdownRequested(): Promise<void> {
851
798
  if (!shutdownState.requested) return;
852
799
 
853
- if (extensionRunner?.hasHandlers("session_shutdown")) {
854
- await extensionRunner.emit({ type: "session_shutdown" });
800
+ if (session.extensionRunner?.hasHandlers("session_shutdown")) {
801
+ await session.extensionRunner.emit({ type: "session_shutdown" });
855
802
  }
856
803
 
857
804
  process.exit(0);
@@ -880,6 +827,11 @@ export async function runRpcMode(
880
827
  continue;
881
828
  }
882
829
 
830
+ if (isRpcHostUriResult(parsed)) {
831
+ hostUriBridge.handleResult(parsed);
832
+ continue;
833
+ }
834
+
883
835
  // Handle regular commands
884
836
  const command = parsed as RpcCommand;
885
837
  const response = await handleCommand(command);
@@ -894,5 +846,6 @@ export async function runRpcMode(
894
846
 
895
847
  // stdin closed — RPC client is gone, exit cleanly
896
848
  hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
849
+ hostUriBridge.clear("RPC client disconnected before host URI request completed");
897
850
  process.exit(0);
898
851
  }
@@ -29,6 +29,7 @@ export type RpcCommand =
29
29
  | { id?: string; type: "get_state" }
30
30
  | { id?: string; type: "set_todos"; phases: TodoPhase[] }
31
31
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
32
+ | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
32
33
 
33
34
  // Model
34
35
  | { id?: string; type: "set_model"; provider: string; modelId: string }
@@ -121,6 +122,7 @@ export type RpcResponse =
121
122
  | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
122
123
  | { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
123
124
  | { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
125
+ | { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
124
126
 
125
127
  // Model
126
128
  | {
@@ -304,6 +306,61 @@ export interface RpcHostToolResult {
304
306
  isError?: boolean;
305
307
  }
306
308
 
309
+ // ============================================================================
310
+ // Host URI Frames (bidirectional)
311
+ // ============================================================================
312
+
313
+ export interface RpcHostUriSchemeDefinition {
314
+ /** URL scheme without trailing `://` (e.g. `db`, `notion`). */
315
+ scheme: string;
316
+ /** Optional human-readable description for logs/diagnostics. */
317
+ description?: string;
318
+ /** When true, the write tool is allowed to dispatch writes to this scheme. */
319
+ writable?: boolean;
320
+ /** When true, downstream callers suppress hashline anchors for resolved content. */
321
+ immutable?: boolean;
322
+ }
323
+
324
+ export type RpcHostUriOperation = "read" | "write";
325
+
326
+ /** Emitted by the RPC server when it needs the host to satisfy a URI operation. */
327
+ export interface RpcHostUriRequest {
328
+ type: "host_uri_request";
329
+ id: string;
330
+ operation: RpcHostUriOperation;
331
+ url: string;
332
+ /** Present for write operations. */
333
+ content?: string;
334
+ }
335
+
336
+ /** Emitted by the RPC server when a pending URI request should be aborted. */
337
+ export interface RpcHostUriCancelRequest {
338
+ type: "host_uri_cancel";
339
+ id: string;
340
+ targetId: string;
341
+ }
342
+
343
+ /** Sent by the host to complete a pending URI request. */
344
+ export interface RpcHostUriResult {
345
+ type: "host_uri_result";
346
+ id: string;
347
+ /**
348
+ * Required for successful `read` results. Ignored for `write` success.
349
+ * Set on errors when a textual explanation accompanies `isError`.
350
+ */
351
+ content?: string;
352
+ /** Defaults to `text/plain` when omitted. */
353
+ contentType?: "text/markdown" | "application/json" | "text/plain";
354
+ /** Optional resolution notes propagated to the read tool. */
355
+ notes?: string[];
356
+ /** Overrides the scheme-level `immutable` flag for this single resolution. */
357
+ immutable?: boolean;
358
+ /** When true, surface the result content as an error to the caller. */
359
+ isError?: boolean;
360
+ /** Optional error message; preferred over `content` for error surfacing. */
361
+ error?: string;
362
+ }
363
+
307
364
  // ============================================================================
308
365
  // Extension UI Commands (stdin)
309
366
  // ============================================================================