@oh-my-pi/pi-coding-agent 13.19.0 → 14.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 +266 -1
- package/package.json +86 -20
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +91 -0
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +83 -125
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -5
- package/src/commit/agentic/index.ts +3 -4
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +3 -4
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +54 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +102 -46
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/settings-defs.ts +5 -0
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +12 -8
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +94 -37
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +2 -2
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +190 -154
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +306 -256
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +21 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -4
- package/src/task/template.ts +2 -2
- package/src/task/worktree.ts +4 -4
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +2 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +10 -11
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +198 -67
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +4 -4
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { Static, TSchema } from "@sinclair/typebox";
|
|
4
|
+
import { applyToolProxy } from "../../extensibility/tool-proxy";
|
|
5
|
+
import type { Theme } from "../../modes/theme/theme";
|
|
6
|
+
import type {
|
|
7
|
+
RpcHostToolCallRequest,
|
|
8
|
+
RpcHostToolCancelRequest,
|
|
9
|
+
RpcHostToolDefinition,
|
|
10
|
+
RpcHostToolResult,
|
|
11
|
+
RpcHostToolUpdate,
|
|
12
|
+
} from "./rpc-types";
|
|
13
|
+
|
|
14
|
+
type RpcHostToolOutput = (frame: RpcHostToolCallRequest | RpcHostToolCancelRequest) => void;
|
|
15
|
+
|
|
16
|
+
type PendingHostToolCall = {
|
|
17
|
+
resolve: (result: AgentToolResult<unknown>) => void;
|
|
18
|
+
reject: (error: Error) => void;
|
|
19
|
+
onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isAgentToolResult(value: unknown): value is AgentToolResult<unknown> {
|
|
23
|
+
if (!value || typeof value !== "object") return false;
|
|
24
|
+
const content = (value as { content?: unknown }).content;
|
|
25
|
+
return Array.isArray(content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
|
|
29
|
+
if (!value || typeof value !== "object") return false;
|
|
30
|
+
const frame = value as { type?: unknown; id?: unknown; result?: unknown };
|
|
31
|
+
return frame.type === "host_tool_result" && typeof frame.id === "string" && isAgentToolResult(frame.result);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
|
|
35
|
+
if (!value || typeof value !== "object") return false;
|
|
36
|
+
const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
|
|
37
|
+
return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme = Theme>
|
|
41
|
+
implements AgentTool<TParams, unknown, TTheme>
|
|
42
|
+
{
|
|
43
|
+
declare name: string;
|
|
44
|
+
declare label: string;
|
|
45
|
+
declare description: string;
|
|
46
|
+
declare parameters: TParams;
|
|
47
|
+
readonly strict = true;
|
|
48
|
+
concurrency: "shared" | "exclusive" = "shared";
|
|
49
|
+
#bridge: RpcHostToolBridge;
|
|
50
|
+
#definition: RpcHostToolDefinition;
|
|
51
|
+
|
|
52
|
+
constructor(definition: RpcHostToolDefinition, bridge: RpcHostToolBridge) {
|
|
53
|
+
this.#definition = definition;
|
|
54
|
+
this.#bridge = bridge;
|
|
55
|
+
applyToolProxy(definition, this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
execute(
|
|
59
|
+
toolCallId: string,
|
|
60
|
+
params: Static<TParams>,
|
|
61
|
+
signal?: AbortSignal,
|
|
62
|
+
onUpdate?: AgentToolUpdateCallback<unknown>,
|
|
63
|
+
): Promise<AgentToolResult<unknown>> {
|
|
64
|
+
return this.#bridge.requestExecution(
|
|
65
|
+
this.#definition,
|
|
66
|
+
toolCallId,
|
|
67
|
+
params as Record<string, unknown>,
|
|
68
|
+
signal,
|
|
69
|
+
onUpdate,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class RpcHostToolBridge {
|
|
75
|
+
#output: RpcHostToolOutput;
|
|
76
|
+
#definitions = new Map<string, RpcHostToolDefinition>();
|
|
77
|
+
#pendingCalls = new Map<string, PendingHostToolCall>();
|
|
78
|
+
|
|
79
|
+
constructor(output: RpcHostToolOutput) {
|
|
80
|
+
this.#output = output;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getToolNames(): string[] {
|
|
84
|
+
return Array.from(this.#definitions.keys());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setTools(tools: RpcHostToolDefinition[]): AgentTool[] {
|
|
88
|
+
this.#definitions = new Map(tools.map(tool => [tool.name, tool]));
|
|
89
|
+
return tools.map(tool => new RpcHostToolAdapter(tool, this));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
handleResult(frame: RpcHostToolResult): boolean {
|
|
93
|
+
const pending = this.#pendingCalls.get(frame.id);
|
|
94
|
+
if (!pending) return false;
|
|
95
|
+
this.#pendingCalls.delete(frame.id);
|
|
96
|
+
if (frame.isError) {
|
|
97
|
+
const text = frame.result.content
|
|
98
|
+
.filter(
|
|
99
|
+
(item): item is { type: "text"; text: string } => item.type === "text" && typeof item.text === "string",
|
|
100
|
+
)
|
|
101
|
+
.map(item => item.text)
|
|
102
|
+
.join("\n")
|
|
103
|
+
.trim();
|
|
104
|
+
pending.reject(new Error(text || "Host tool execution failed"));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
pending.resolve(frame.result);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
handleUpdate(frame: RpcHostToolUpdate): boolean {
|
|
112
|
+
const pending = this.#pendingCalls.get(frame.id);
|
|
113
|
+
if (!pending) return false;
|
|
114
|
+
pending.onUpdate?.(frame.partialResult);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
requestExecution(
|
|
119
|
+
definition: RpcHostToolDefinition,
|
|
120
|
+
toolCallId: string,
|
|
121
|
+
args: Record<string, unknown>,
|
|
122
|
+
signal?: AbortSignal,
|
|
123
|
+
onUpdate?: AgentToolUpdateCallback<unknown>,
|
|
124
|
+
): Promise<AgentToolResult<unknown>> {
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const id = Snowflake.next() as string;
|
|
130
|
+
const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
|
|
131
|
+
let settled = false;
|
|
132
|
+
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
signal?.removeEventListener("abort", onAbort);
|
|
135
|
+
this.#pendingCalls.delete(id);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const onAbort = () => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
settled = true;
|
|
141
|
+
cleanup();
|
|
142
|
+
this.#output({
|
|
143
|
+
type: "host_tool_cancel",
|
|
144
|
+
id: Snowflake.next() as string,
|
|
145
|
+
targetId: id,
|
|
146
|
+
});
|
|
147
|
+
reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
this.#pendingCalls.set(id, {
|
|
152
|
+
resolve: result => {
|
|
153
|
+
if (settled) return;
|
|
154
|
+
settled = true;
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve(result);
|
|
157
|
+
},
|
|
158
|
+
reject: error => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settled = true;
|
|
161
|
+
cleanup();
|
|
162
|
+
reject(error);
|
|
163
|
+
},
|
|
164
|
+
onUpdate,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.#output({
|
|
168
|
+
type: "host_tool_call",
|
|
169
|
+
id,
|
|
170
|
+
toolCallId,
|
|
171
|
+
toolName: definition.name,
|
|
172
|
+
arguments: args,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return promise;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
rejectAllPending(message: string): void {
|
|
179
|
+
const error = new Error(message);
|
|
180
|
+
const pendingCalls = Array.from(this.#pendingCalls.values());
|
|
181
|
+
this.#pendingCalls.clear();
|
|
182
|
+
for (const pending of pendingCalls) {
|
|
183
|
+
pending.reject(error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
|
5
5
|
*/
|
|
6
|
-
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { BashResult } from "../../exec/bash-executor";
|
|
10
10
|
import type { SessionStats } from "../../session/agent-session";
|
|
11
11
|
import type { CompactionResult } from "../../session/compaction";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
RpcCommand,
|
|
14
|
+
RpcHostToolCallRequest,
|
|
15
|
+
RpcHostToolCancelRequest,
|
|
16
|
+
RpcHostToolDefinition,
|
|
17
|
+
RpcHostToolResult,
|
|
18
|
+
RpcHostToolUpdate,
|
|
19
|
+
RpcResponse,
|
|
20
|
+
RpcSessionState,
|
|
21
|
+
} from "./rpc-types";
|
|
13
22
|
|
|
14
23
|
/** Distributive Omit that works with union types */
|
|
15
24
|
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
@@ -32,12 +41,40 @@ export interface RpcClientOptions {
|
|
|
32
41
|
sessionDir?: string;
|
|
33
42
|
/** Additional CLI arguments */
|
|
34
43
|
args?: string[];
|
|
44
|
+
/** Custom tools owned by the embedding host and exposed over the RPC transport */
|
|
45
|
+
customTools?: RpcClientCustomTool[];
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
|
|
38
49
|
|
|
39
50
|
export type RpcEventListener = (event: AgentEvent) => void;
|
|
40
51
|
|
|
52
|
+
export interface RpcClientToolContext<TDetails = unknown> {
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
signal: AbortSignal;
|
|
55
|
+
sendUpdate(partialResult: RpcClientToolResult<TDetails>): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type RpcClientToolResult<TDetails = unknown> = AgentToolResult<TDetails> | string;
|
|
59
|
+
|
|
60
|
+
export interface RpcClientCustomTool<
|
|
61
|
+
TParams extends Record<string, unknown> = Record<string, unknown>,
|
|
62
|
+
TDetails = unknown,
|
|
63
|
+
> extends Omit<RpcHostToolDefinition, "parameters"> {
|
|
64
|
+
parameters: Record<string, unknown>;
|
|
65
|
+
execute(
|
|
66
|
+
params: TParams,
|
|
67
|
+
context: RpcClientToolContext<TDetails>,
|
|
68
|
+
): Promise<RpcClientToolResult<TDetails>> | RpcClientToolResult<TDetails>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function defineRpcClientTool<
|
|
72
|
+
TParams extends Record<string, unknown> = Record<string, unknown>,
|
|
73
|
+
TDetails = unknown,
|
|
74
|
+
>(tool: RpcClientCustomTool<TParams, TDetails>): RpcClientCustomTool<TParams, TDetails> {
|
|
75
|
+
return tool;
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
42
79
|
"agent_start",
|
|
43
80
|
"agent_end",
|
|
@@ -70,6 +107,31 @@ function isAgentEvent(value: unknown): value is AgentEvent {
|
|
|
70
107
|
return agentEventTypes.has(type as AgentEvent["type"]);
|
|
71
108
|
}
|
|
72
109
|
|
|
110
|
+
function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
|
|
111
|
+
if (!isRecord(value)) return false;
|
|
112
|
+
return (
|
|
113
|
+
value.type === "host_tool_call" &&
|
|
114
|
+
typeof value.id === "string" &&
|
|
115
|
+
typeof value.toolCallId === "string" &&
|
|
116
|
+
typeof value.toolName === "string" &&
|
|
117
|
+
isRecord(value.arguments)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isRpcHostToolCancelRequest(value: unknown): value is RpcHostToolCancelRequest {
|
|
122
|
+
if (!isRecord(value)) return false;
|
|
123
|
+
return value.type === "host_tool_cancel" && typeof value.id === "string" && typeof value.targetId === "string";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
|
|
127
|
+
if (typeof result === "string") {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: result }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
73
135
|
// ============================================================================
|
|
74
136
|
// RPC Client
|
|
75
137
|
// ============================================================================
|
|
@@ -79,10 +141,14 @@ export class RpcClient {
|
|
|
79
141
|
#eventListeners: RpcEventListener[] = [];
|
|
80
142
|
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
81
143
|
new Map();
|
|
144
|
+
#customTools: RpcClientCustomTool[] = [];
|
|
145
|
+
#pendingHostToolCalls = new Map<string, { controller: AbortController }>();
|
|
82
146
|
#requestId = 0;
|
|
83
147
|
#abortController = new AbortController();
|
|
84
148
|
|
|
85
|
-
constructor(private options: RpcClientOptions = {}) {
|
|
149
|
+
constructor(private options: RpcClientOptions = {}) {
|
|
150
|
+
this.#customTools = [...(options.customTools ?? [])];
|
|
151
|
+
}
|
|
86
152
|
|
|
87
153
|
/**
|
|
88
154
|
* Start the RPC agent process.
|
|
@@ -162,6 +228,9 @@ export class RpcClient {
|
|
|
162
228
|
|
|
163
229
|
try {
|
|
164
230
|
await readyPromise;
|
|
231
|
+
if (this.#customTools.length > 0) {
|
|
232
|
+
await this.setCustomTools(this.#customTools);
|
|
233
|
+
}
|
|
165
234
|
} finally {
|
|
166
235
|
clearTimeout(readyTimeout);
|
|
167
236
|
}
|
|
@@ -177,6 +246,10 @@ export class RpcClient {
|
|
|
177
246
|
this.#abortController.abort();
|
|
178
247
|
this.#process = null;
|
|
179
248
|
this.#pendingRequests.clear();
|
|
249
|
+
for (const pendingCall of this.#pendingHostToolCalls.values()) {
|
|
250
|
+
pendingCall.controller.abort();
|
|
251
|
+
}
|
|
252
|
+
this.#pendingHostToolCalls.clear();
|
|
180
253
|
}
|
|
181
254
|
|
|
182
255
|
/**
|
|
@@ -434,6 +507,26 @@ export class RpcClient {
|
|
|
434
507
|
return this.#getData<{ messages: AgentMessage[] }>(response).messages;
|
|
435
508
|
}
|
|
436
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Replace the host-owned custom tools exposed to the RPC session.
|
|
512
|
+
* Changes take effect before the next model call.
|
|
513
|
+
*/
|
|
514
|
+
async setCustomTools(tools: RpcClientCustomTool[]): Promise<string[]> {
|
|
515
|
+
this.#customTools = [...tools];
|
|
516
|
+
if (!this.#process) {
|
|
517
|
+
return this.#customTools.map(tool => tool.name);
|
|
518
|
+
}
|
|
519
|
+
const definitions: RpcHostToolDefinition[] = this.#customTools.map(tool => ({
|
|
520
|
+
name: tool.name,
|
|
521
|
+
label: tool.label,
|
|
522
|
+
description: tool.description,
|
|
523
|
+
parameters: tool.parameters,
|
|
524
|
+
hidden: tool.hidden,
|
|
525
|
+
}));
|
|
526
|
+
const response = await this.#send({ type: "set_host_tools", tools: definitions });
|
|
527
|
+
return this.#getData<{ toolNames: string[] }>(response).toolNames;
|
|
528
|
+
}
|
|
529
|
+
|
|
437
530
|
// =========================================================================
|
|
438
531
|
// Helpers
|
|
439
532
|
// =========================================================================
|
|
@@ -514,6 +607,16 @@ export class RpcClient {
|
|
|
514
607
|
}
|
|
515
608
|
}
|
|
516
609
|
|
|
610
|
+
if (isRpcHostToolCallRequest(data)) {
|
|
611
|
+
void this.#handleHostToolCall(data);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (isRpcHostToolCancelRequest(data)) {
|
|
616
|
+
this.#pendingHostToolCalls.get(data.targetId)?.controller.abort();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
517
620
|
if (!isAgentEvent(data)) return;
|
|
518
621
|
|
|
519
622
|
// Otherwise it's an event
|
|
@@ -555,21 +658,83 @@ export class RpcClient {
|
|
|
555
658
|
},
|
|
556
659
|
});
|
|
557
660
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
661
|
+
this.#writeFrame(fullCommand, err => {
|
|
662
|
+
this.#pendingRequests.delete(id);
|
|
663
|
+
if (settled) return;
|
|
664
|
+
settled = true;
|
|
665
|
+
clearTimeout(timeoutId);
|
|
666
|
+
reject(err);
|
|
667
|
+
});
|
|
668
|
+
return promise;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async #handleHostToolCall(request: RpcHostToolCallRequest): Promise<void> {
|
|
672
|
+
const tool = this.#customTools.find(candidate => candidate.name === request.toolName);
|
|
673
|
+
if (!tool) {
|
|
674
|
+
this.#writeFrame({
|
|
675
|
+
type: "host_tool_result",
|
|
676
|
+
id: request.id,
|
|
677
|
+
result: {
|
|
678
|
+
content: [{ type: "text", text: `Host tool "${request.toolName}" is not registered` }],
|
|
679
|
+
details: {},
|
|
680
|
+
},
|
|
681
|
+
isError: true,
|
|
682
|
+
} satisfies RpcHostToolResult);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const controller = new AbortController();
|
|
687
|
+
this.#pendingHostToolCalls.set(request.id, { controller });
|
|
688
|
+
|
|
689
|
+
const sendUpdate = (partialResult: RpcClientToolResult<unknown>): void => {
|
|
690
|
+
if (controller.signal.aborted) return;
|
|
691
|
+
this.#writeFrame({
|
|
692
|
+
type: "host_tool_update",
|
|
693
|
+
id: request.id,
|
|
694
|
+
partialResult: normalizeToolResult(partialResult),
|
|
695
|
+
} satisfies RpcHostToolUpdate);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const result = await tool.execute(request.arguments, {
|
|
700
|
+
toolCallId: request.toolCallId,
|
|
701
|
+
signal: controller.signal,
|
|
702
|
+
sendUpdate,
|
|
703
|
+
});
|
|
704
|
+
if (controller.signal.aborted) return;
|
|
705
|
+
this.#writeFrame({
|
|
706
|
+
type: "host_tool_result",
|
|
707
|
+
id: request.id,
|
|
708
|
+
result: normalizeToolResult(result),
|
|
709
|
+
} satisfies RpcHostToolResult);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (controller.signal.aborted) return;
|
|
712
|
+
this.#writeFrame({
|
|
713
|
+
type: "host_tool_result",
|
|
714
|
+
id: request.id,
|
|
715
|
+
result: {
|
|
716
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
717
|
+
details: {},
|
|
718
|
+
},
|
|
719
|
+
isError: true,
|
|
720
|
+
} satisfies RpcHostToolResult);
|
|
721
|
+
} finally {
|
|
722
|
+
this.#pendingHostToolCalls.delete(request.id);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
#writeFrame(frame: RpcCommand | RpcHostToolResult | RpcHostToolUpdate, onError?: (error: Error) => void): void {
|
|
727
|
+
if (!this.#process?.stdin) {
|
|
728
|
+
throw new Error("Client not started");
|
|
729
|
+
}
|
|
730
|
+
const stdin = this.#process.stdin as import("bun").FileSink;
|
|
731
|
+
stdin.write(`${JSON.stringify(frame)}\n`);
|
|
562
732
|
const flushResult = stdin.flush();
|
|
563
733
|
if (flushResult instanceof Promise) {
|
|
564
734
|
flushResult.catch((err: Error) => {
|
|
565
|
-
|
|
566
|
-
if (settled) return;
|
|
567
|
-
settled = true;
|
|
568
|
-
clearTimeout(timeoutId);
|
|
569
|
-
reject(err);
|
|
735
|
+
onError?.(err);
|
|
570
736
|
});
|
|
571
737
|
}
|
|
572
|
-
return promise;
|
|
573
738
|
}
|
|
574
739
|
|
|
575
740
|
#getData<T>(response: RpcResponse): T {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
-
import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import type {
|
|
15
15
|
ExtensionUIContext,
|
|
16
16
|
ExtensionUIDialogOptions,
|
|
@@ -18,10 +18,14 @@ import type {
|
|
|
18
18
|
} from "../../extensibility/extensions";
|
|
19
19
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
20
20
|
import type { AgentSession } from "../../session/agent-session";
|
|
21
|
+
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
21
22
|
import type {
|
|
22
23
|
RpcCommand,
|
|
23
24
|
RpcExtensionUIRequest,
|
|
24
25
|
RpcExtensionUIResponse,
|
|
26
|
+
RpcHostToolCallRequest,
|
|
27
|
+
RpcHostToolCancelRequest,
|
|
28
|
+
RpcHostToolDefinition,
|
|
25
29
|
RpcResponse,
|
|
26
30
|
RpcSessionState,
|
|
27
31
|
} from "./rpc-types";
|
|
@@ -34,7 +38,40 @@ export type PendingExtensionRequest = {
|
|
|
34
38
|
reject: (error: Error) => void;
|
|
35
39
|
};
|
|
36
40
|
|
|
37
|
-
type RpcOutput = (
|
|
41
|
+
type RpcOutput = (
|
|
42
|
+
obj: RpcResponse | RpcExtensionUIRequest | RpcHostToolCallRequest | RpcHostToolCancelRequest | object,
|
|
43
|
+
) => void;
|
|
44
|
+
|
|
45
|
+
function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
|
|
46
|
+
return tools.map((tool, index) => {
|
|
47
|
+
const name = typeof tool.name === "string" ? tool.name.trim() : "";
|
|
48
|
+
if (!name) {
|
|
49
|
+
throw new Error(`Host tool at index ${index} must provide a non-empty name`);
|
|
50
|
+
}
|
|
51
|
+
const description = typeof tool.description === "string" ? tool.description.trim() : "";
|
|
52
|
+
if (!description) {
|
|
53
|
+
throw new Error(`Host tool "${name}" must provide a non-empty description`);
|
|
54
|
+
}
|
|
55
|
+
if (!tool.parameters || typeof tool.parameters !== "object" || Array.isArray(tool.parameters)) {
|
|
56
|
+
throw new Error(`Host tool "${name}" must provide a JSON Schema object`);
|
|
57
|
+
}
|
|
58
|
+
const label = typeof tool.label === "string" && tool.label.trim() ? tool.label.trim() : name;
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
label,
|
|
62
|
+
description,
|
|
63
|
+
parameters: tool.parameters,
|
|
64
|
+
hidden: tool.hidden === true,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldEmitRpcTitles(): boolean {
|
|
70
|
+
const raw = $env.PI_RPC_EMIT_TITLE;
|
|
71
|
+
if (!raw) return false;
|
|
72
|
+
const normalized = raw.trim().toLowerCase();
|
|
73
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
74
|
+
}
|
|
38
75
|
|
|
39
76
|
export function requestRpcEditor(
|
|
40
77
|
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
@@ -110,6 +147,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
110
147
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
111
148
|
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
112
149
|
};
|
|
150
|
+
const emitRpcTitles = shouldEmitRpcTitles();
|
|
113
151
|
|
|
114
152
|
const success = <T extends RpcCommand["type"]>(
|
|
115
153
|
id: string | undefined,
|
|
@@ -127,6 +165,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
127
165
|
};
|
|
128
166
|
|
|
129
167
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
168
|
+
const hostToolBridge = new RpcHostToolBridge(output);
|
|
130
169
|
|
|
131
170
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
132
171
|
const shutdownState = { requested: false };
|
|
@@ -291,7 +330,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
291
330
|
}
|
|
292
331
|
|
|
293
332
|
setTitle(title: string): void {
|
|
294
|
-
//
|
|
333
|
+
// Title updates are low-value noise for most RPC hosts; opt in via PI_RPC_EMIT_TITLE=1.
|
|
334
|
+
if (!emitRpcTitles) return;
|
|
295
335
|
this.output({
|
|
296
336
|
type: "extension_ui_request",
|
|
297
337
|
id: Snowflake.next() as string,
|
|
@@ -544,10 +584,29 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
544
584
|
autoCompactionEnabled: session.autoCompactionEnabled,
|
|
545
585
|
messageCount: session.messages.length,
|
|
546
586
|
queuedMessageCount: session.queuedMessageCount,
|
|
587
|
+
todoPhases: session.getTodoPhases(),
|
|
588
|
+
systemPrompt: session.systemPrompt,
|
|
589
|
+
dumpTools: session.agent.state.tools.map(tool => ({
|
|
590
|
+
name: tool.name,
|
|
591
|
+
description: tool.description,
|
|
592
|
+
parameters: tool.parameters,
|
|
593
|
+
})),
|
|
547
594
|
};
|
|
548
595
|
return success(id, "get_state", state);
|
|
549
596
|
}
|
|
550
597
|
|
|
598
|
+
case "set_todos": {
|
|
599
|
+
session.setTodoPhases(command.phases);
|
|
600
|
+
return success(id, "set_todos", { todoPhases: session.getTodoPhases() });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
case "set_host_tools": {
|
|
604
|
+
const tools = normalizeHostToolDefinitions(command.tools);
|
|
605
|
+
const rpcTools = hostToolBridge.setTools(tools);
|
|
606
|
+
await session.refreshRpcHostTools(rpcTools);
|
|
607
|
+
return success(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
|
|
608
|
+
}
|
|
609
|
+
|
|
551
610
|
// =================================================================
|
|
552
611
|
// Model
|
|
553
612
|
// =================================================================
|
|
@@ -738,6 +797,16 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
738
797
|
continue;
|
|
739
798
|
}
|
|
740
799
|
|
|
800
|
+
if (isRpcHostToolResult(parsed)) {
|
|
801
|
+
hostToolBridge.handleResult(parsed);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (isRpcHostToolUpdate(parsed)) {
|
|
806
|
+
hostToolBridge.handleUpdate(parsed);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
741
810
|
// Handle regular commands
|
|
742
811
|
const command = parsed as RpcCommand;
|
|
743
812
|
const response = await handleCommand(command);
|
|
@@ -751,5 +820,6 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
751
820
|
}
|
|
752
821
|
|
|
753
822
|
// stdin closed — RPC client is gone, exit cleanly
|
|
823
|
+
hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
|
|
754
824
|
process.exit(0);
|
|
755
825
|
}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Commands are sent as JSON lines on stdin.
|
|
5
5
|
* Responses and events are emitted as JSON lines on stdout.
|
|
6
6
|
*/
|
|
7
|
-
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
|
+
import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type { BashResult } from "../../exec/bash-executor";
|
|
10
10
|
import type { SessionStats } from "../../session/agent-session";
|
|
11
11
|
import type { CompactionResult } from "../../session/compaction";
|
|
12
|
+
import type { TodoPhase } from "../../tools/todo-write";
|
|
12
13
|
|
|
13
14
|
// ============================================================================
|
|
14
15
|
// RPC Commands (stdin)
|
|
@@ -25,6 +26,8 @@ export type RpcCommand =
|
|
|
25
26
|
|
|
26
27
|
// State
|
|
27
28
|
| { id?: string; type: "get_state" }
|
|
29
|
+
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
30
|
+
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
28
31
|
|
|
29
32
|
// Model
|
|
30
33
|
| { id?: string; type: "set_model"; provider: string; modelId: string }
|
|
@@ -82,6 +85,10 @@ export interface RpcSessionState {
|
|
|
82
85
|
autoCompactionEnabled: boolean;
|
|
83
86
|
messageCount: number;
|
|
84
87
|
queuedMessageCount: number;
|
|
88
|
+
todoPhases: TodoPhase[];
|
|
89
|
+
/** For session dump / export (plain-text parity with /dump). */
|
|
90
|
+
systemPrompt?: string;
|
|
91
|
+
dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
// ============================================================================
|
|
@@ -100,6 +107,8 @@ export type RpcResponse =
|
|
|
100
107
|
|
|
101
108
|
// State
|
|
102
109
|
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
|
|
110
|
+
| { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
|
|
111
|
+
| { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
|
|
103
112
|
|
|
104
113
|
// Model
|
|
105
114
|
| {
|
|
@@ -228,6 +237,49 @@ export type RpcExtensionUIRequest =
|
|
|
228
237
|
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
229
238
|
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };
|
|
230
239
|
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Host Tool Frames (bidirectional)
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
export interface RpcHostToolDefinition {
|
|
245
|
+
name: string;
|
|
246
|
+
label?: string;
|
|
247
|
+
description: string;
|
|
248
|
+
parameters: Record<string, unknown>;
|
|
249
|
+
hidden?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Emitted by the RPC server when it needs the host to execute a registered tool. */
|
|
253
|
+
export interface RpcHostToolCallRequest {
|
|
254
|
+
type: "host_tool_call";
|
|
255
|
+
id: string;
|
|
256
|
+
toolCallId: string;
|
|
257
|
+
toolName: string;
|
|
258
|
+
arguments: Record<string, unknown>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Emitted by the RPC server when a pending host tool call should be aborted. */
|
|
262
|
+
export interface RpcHostToolCancelRequest {
|
|
263
|
+
type: "host_tool_cancel";
|
|
264
|
+
id: string;
|
|
265
|
+
targetId: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Sent by the host to stream partial tool updates back to the RPC server. */
|
|
269
|
+
export interface RpcHostToolUpdate {
|
|
270
|
+
type: "host_tool_update";
|
|
271
|
+
id: string;
|
|
272
|
+
partialResult: AgentToolResult<unknown>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Sent by the host to complete a pending tool call. */
|
|
276
|
+
export interface RpcHostToolResult {
|
|
277
|
+
type: "host_tool_result";
|
|
278
|
+
id: string;
|
|
279
|
+
result: AgentToolResult<unknown>;
|
|
280
|
+
isError?: boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
231
283
|
// ============================================================================
|
|
232
284
|
// Extension UI Commands (stdin)
|
|
233
285
|
// ============================================================================
|