@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.
- package/CHANGELOG.md +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/modes/print-mode.ts
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
// ============================================================================
|