@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.0
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 +75 -5
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-broker-cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +42 -1
- package/dist/types/edit/index.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +9 -1
- package/dist/types/mcp/client.d.ts +2 -1
- package/dist/types/mcp/oauth-discovery.d.ts +4 -3
- package/dist/types/mcp/timeout.d.ts +9 -0
- package/dist/types/mcp/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/approval.d.ts +46 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +11 -1
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/calculator.d.ts +1 -0
- package/dist/types/tools/checkpoint.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +9 -1
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +10 -0
- package/dist/types/tools/gh.d.ts +2 -1
- package/dist/types/tools/hindsight-recall.d.ts +1 -0
- package/dist/types/tools/hindsight-reflect.d.ts +1 -0
- package/dist/types/tools/hindsight-retain.d.ts +1 -0
- package/dist/types/tools/inspect-image.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tools/recipe/index.d.ts +1 -0
- package/dist/types/tools/render-mermaid.d.ts +1 -0
- package/dist/types/tools/resolve.d.ts +1 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -0
- package/dist/types/tools/search.d.ts +1 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo-write.d.ts +1 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tools/yield.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +14 -0
- package/src/cli/auth-broker-cli.ts +171 -22
- package/src/commands/auth-broker.ts +3 -0
- package/src/commands/launch.ts +16 -0
- package/src/config/mcp-schema.json +2 -2
- package/src/config/model-registry.ts +19 -4
- package/src/config/settings-schema.ts +59 -1
- package/src/config/settings.ts +2 -1
- package/src/dap/session.ts +35 -2
- package/src/discovery/builtin.ts +2 -2
- package/src/discovery/mcp-json.ts +1 -1
- package/src/edit/index.ts +26 -0
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/streaming.ts +12 -2
- package/src/exec/bash-executor.ts +6 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
- package/src/extensibility/custom-tools/types.ts +16 -2
- package/src/extensibility/extensions/wrapper.ts +36 -1
- package/src/extensibility/hooks/types.ts +8 -1
- package/src/hashline/apply.ts +47 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/edits.ts +82 -29
- package/src/lsp/index.ts +38 -1
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +6 -0
- package/src/mcp/client.ts +8 -6
- package/src/mcp/oauth-discovery.ts +120 -32
- package/src/mcp/oauth-flow.ts +34 -6
- package/src/mcp/timeout.ts +59 -0
- package/src/mcp/transports/http.ts +42 -44
- package/src/mcp/transports/stdio.ts +8 -5
- package/src/mcp/types.ts +1 -1
- package/src/modes/components/hook-editor.ts +11 -3
- package/src/modes/components/mcp-add-wizard.ts +6 -2
- package/src/modes/components/model-selector.ts +33 -11
- package/src/modes/controllers/command-controller.ts +6 -4
- package/src/modes/controllers/mcp-command-controller.ts +8 -4
- package/src/prompts/review-custom-request.md +22 -0
- package/src/prompts/review-headless-request.md +16 -0
- package/src/prompts/review-request.md +2 -3
- package/src/prompts/system/project-prompt.md +4 -0
- package/src/prompts/tools/debug.md +1 -0
- package/src/prompts/tools/find.md +4 -2
- package/src/prompts/tools/hashline.md +1 -0
- package/src/sdk.ts +47 -73
- package/src/session/agent-session.ts +93 -27
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +19 -0
- package/src/task/render.ts +12 -2
- package/src/task/types.ts +4 -0
- package/src/tools/approval.ts +185 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit.ts +25 -1
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +69 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser.ts +15 -0
- package/src/tools/calculator.ts +1 -0
- package/src/tools/checkpoint.ts +2 -0
- package/src/tools/debug.ts +38 -0
- package/src/tools/eval.ts +15 -0
- package/src/tools/find.ts +17 -8
- package/src/tools/gh.ts +21 -1
- package/src/tools/hindsight-recall.ts +1 -0
- package/src/tools/hindsight-reflect.ts +1 -0
- package/src/tools/hindsight-retain.ts +1 -0
- package/src/tools/image-gen.ts +1 -0
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +1 -0
- package/src/tools/job.ts +1 -0
- package/src/tools/path-utils.ts +14 -1
- package/src/tools/read.ts +1 -0
- package/src/tools/recipe/index.ts +1 -0
- package/src/tools/render-mermaid.ts +1 -0
- package/src/tools/report-tool-issue.ts +1 -0
- package/src/tools/resolve.ts +1 -0
- package/src/tools/review.ts +1 -0
- package/src/tools/search-tool-bm25.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/ssh.ts +8 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +12 -1
- package/src/tools/yield.ts +1 -0
- package/src/web/search/index.ts +2 -0
|
@@ -1574,7 +1574,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
1574
1574
|
ui: {
|
|
1575
1575
|
tab: "editing",
|
|
1576
1576
|
label: "Hashline Duplicate Insert Drop",
|
|
1577
|
-
description:
|
|
1577
|
+
description:
|
|
1578
|
+
"Drop payload lines that duplicate adjacent file context — 2+-line context echoes on `↑`/`↓` inserts, and a single boundary line at either edge of an `A-B:` replacement",
|
|
1578
1579
|
},
|
|
1579
1580
|
},
|
|
1580
1581
|
"edit.blockAutoGenerated": {
|
|
@@ -1789,6 +1790,53 @@ export const SETTINGS_SCHEMA = {
|
|
|
1789
1790
|
// Tools
|
|
1790
1791
|
// ────────────────────────────────────────────────────────────────────────
|
|
1791
1792
|
|
|
1793
|
+
// Tool approval policies
|
|
1794
|
+
"tools.approval": {
|
|
1795
|
+
type: "record",
|
|
1796
|
+
default: {},
|
|
1797
|
+
ui: {
|
|
1798
|
+
tab: "tools",
|
|
1799
|
+
label: "Tool Approval Policies",
|
|
1800
|
+
description:
|
|
1801
|
+
"Per-tool approval policies. Set to 'allow' to auto-approve, 'prompt' to require confirmation, or 'deny' to block. Overrides are honored in every approval mode.",
|
|
1802
|
+
},
|
|
1803
|
+
},
|
|
1804
|
+
|
|
1805
|
+
// Default tool approval mode (interaction tab, but governs the tool wrapper).
|
|
1806
|
+
// "always-ask" — auto-approves read-tier tools only; prompts for write/exec.
|
|
1807
|
+
// "write" — auto-approves read and write-tier tools; prompts for exec.
|
|
1808
|
+
// "yolo" — auto-approves every tier unless a tool declares `override: true`.
|
|
1809
|
+
"tools.approvalMode": {
|
|
1810
|
+
type: "enum",
|
|
1811
|
+
values: ["always-ask", "write", "yolo"] as const,
|
|
1812
|
+
default: "yolo",
|
|
1813
|
+
ui: {
|
|
1814
|
+
tab: "interaction",
|
|
1815
|
+
label: "Tool Approval",
|
|
1816
|
+
description:
|
|
1817
|
+
"Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves every tier unless a tool declares a safety override. `tools.approval.<tool>` overrides are honored in every mode.",
|
|
1818
|
+
options: [
|
|
1819
|
+
{
|
|
1820
|
+
value: "always-ask",
|
|
1821
|
+
label: "Always ask",
|
|
1822
|
+
description: "Auto-approve read-only tools; require confirmation for write and exec tools.",
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
value: "write",
|
|
1826
|
+
label: "Write",
|
|
1827
|
+
description:
|
|
1828
|
+
"Auto-approve read-only and write tools; require confirmation for exec tools such as bash, eval, browser, task, recipe, and ssh.",
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
value: "yolo",
|
|
1832
|
+
label: "Yolo",
|
|
1833
|
+
description:
|
|
1834
|
+
"Auto-approve read, write, and exec tools. Safety overrides declared by tools (for example critical bash patterns) still require confirmation.",
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
|
|
1792
1840
|
// Todo tool
|
|
1793
1841
|
"todo.enabled": {
|
|
1794
1842
|
type: "boolean",
|
|
@@ -2477,6 +2525,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
2477
2525
|
},
|
|
2478
2526
|
},
|
|
2479
2527
|
|
|
2528
|
+
"task.showResolvedModelBadge": {
|
|
2529
|
+
type: "boolean",
|
|
2530
|
+
default: false,
|
|
2531
|
+
ui: {
|
|
2532
|
+
tab: "appearance",
|
|
2533
|
+
label: "Show Resolved Model Badge",
|
|
2534
|
+
description: "Display the actual model ID used by each subagent in the task widget status line",
|
|
2535
|
+
},
|
|
2536
|
+
},
|
|
2537
|
+
|
|
2480
2538
|
// Skills
|
|
2481
2539
|
"skills.enabled": { type: "boolean", default: true },
|
|
2482
2540
|
|
package/src/config/settings.ts
CHANGED
|
@@ -100,7 +100,6 @@ function setByPath(obj: RawSettings, segments: string[], value: unknown): void {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disabledProviders"]);
|
|
103
|
-
|
|
104
103
|
type PathScopedStringArrayEntry = {
|
|
105
104
|
path?: unknown;
|
|
106
105
|
paths?: unknown;
|
|
@@ -218,6 +217,8 @@ export class Settings {
|
|
|
218
217
|
for (const [key, value] of Object.entries(options.overrides)) {
|
|
219
218
|
setByPath(this.#overrides, key.split("."), value);
|
|
220
219
|
}
|
|
220
|
+
|
|
221
|
+
this.#overrides = this.#migrateRawSettings(this.#overrides);
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
package/src/dap/session.ts
CHANGED
|
@@ -108,14 +108,28 @@ function toErrorMessage(value: unknown): string {
|
|
|
108
108
|
interface DapStartRequestFailure {
|
|
109
109
|
rejected: boolean;
|
|
110
110
|
error?: unknown;
|
|
111
|
+
/**
|
|
112
|
+
* Resolves (never rejects) when the underlying launch/attach request
|
|
113
|
+
* settles either way. Set by {@link trackDapStartRequest} on each call,
|
|
114
|
+
* so a single failure object must not be reused across launch attempts.
|
|
115
|
+
* Consumed by {@link throwPreferredDapStartError} to bound how long to
|
|
116
|
+
* wait for a delayed adapter-side rejection before falling back to the
|
|
117
|
+
* cascade error from configurationDone.
|
|
118
|
+
*/
|
|
119
|
+
settled?: Promise<void>;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
function trackDapStartRequest<T>(promise: Promise<T>, failure: DapStartRequestFailure): Promise<T> {
|
|
114
|
-
|
|
123
|
+
const tracked = promise.catch(error => {
|
|
115
124
|
failure.rejected = true;
|
|
116
125
|
failure.error = error;
|
|
117
126
|
throw error;
|
|
118
127
|
});
|
|
128
|
+
failure.settled = tracked.then(
|
|
129
|
+
() => {},
|
|
130
|
+
() => {},
|
|
131
|
+
);
|
|
132
|
+
return tracked;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
function combineDapStartErrors(command: "launch" | "attach", startError: unknown, configurationError: unknown): Error {
|
|
@@ -134,12 +148,27 @@ async function throwPreferredDapStartError(
|
|
|
134
148
|
startFailure: DapStartRequestFailure,
|
|
135
149
|
configurationError: unknown,
|
|
136
150
|
): Promise<never> {
|
|
137
|
-
await Promise.resolve();
|
|
151
|
+
await Promise.race([startFailure.settled ?? Promise.resolve(), timers.setTimeout(50)]);
|
|
138
152
|
if (startFailure.rejected) {
|
|
139
153
|
throw combineDapStartErrors(command, startFailure.error, configurationError);
|
|
140
154
|
}
|
|
141
155
|
throw configurationError;
|
|
142
156
|
}
|
|
157
|
+
|
|
158
|
+
const DEBUGPY_MISSING_MODULE_RE = /No module named ['"]?debugpy['"]?/;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Map a generic adapter-side failure into the targeted `pip install debugpy`
|
|
162
|
+
* hint when the adapter is debugpy and stderr/the wrapping error mentions
|
|
163
|
+
* the missing module. Returns null when the heuristic does not apply, so the
|
|
164
|
+
* caller can rethrow the original error untouched.
|
|
165
|
+
*/
|
|
166
|
+
function mapDebugpyMissingModule(adapterName: string, error: unknown): Error | null {
|
|
167
|
+
if (adapterName !== "debugpy") return null;
|
|
168
|
+
if (!DEBUGPY_MISSING_MODULE_RE.test(toErrorMessage(error))) return null;
|
|
169
|
+
return new Error("adapter 'debugpy' is not available: install with 'pip install debugpy'");
|
|
170
|
+
}
|
|
171
|
+
|
|
143
172
|
function normalizePath(filePath: string): string {
|
|
144
173
|
return path.resolve(filePath);
|
|
145
174
|
}
|
|
@@ -274,6 +303,8 @@ export class DapSessionManager {
|
|
|
274
303
|
return buildSummary(session);
|
|
275
304
|
} catch (error) {
|
|
276
305
|
await this.#disposeSession(session);
|
|
306
|
+
const mapped = mapDebugpyMissingModule(options.adapter.name, error);
|
|
307
|
+
if (mapped) throw mapped;
|
|
277
308
|
throw error;
|
|
278
309
|
}
|
|
279
310
|
}
|
|
@@ -330,6 +361,8 @@ export class DapSessionManager {
|
|
|
330
361
|
return buildSummary(session);
|
|
331
362
|
} catch (error) {
|
|
332
363
|
await this.#disposeSession(session);
|
|
364
|
+
const mapped = mapDebugpyMissingModule(options.adapter.name, error);
|
|
365
|
+
if (mapped) throw mapped;
|
|
333
366
|
throw error;
|
|
334
367
|
}
|
|
335
368
|
}
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -132,7 +132,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
132
132
|
if (serverConfig.timeout === undefined || serverConfig.timeout === null) {
|
|
133
133
|
timeout = undefined;
|
|
134
134
|
} else if (typeof serverConfig.timeout === "number") {
|
|
135
|
-
if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout
|
|
135
|
+
if (Number.isFinite(serverConfig.timeout) && serverConfig.timeout >= 0) {
|
|
136
136
|
timeout = serverConfig.timeout;
|
|
137
137
|
} else {
|
|
138
138
|
logger.warn(`MCP server "${serverName}": invalid timeout ${serverConfig.timeout}, ignoring`);
|
|
@@ -140,7 +140,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
140
140
|
}
|
|
141
141
|
} else if (typeof serverConfig.timeout === "string") {
|
|
142
142
|
const parsed = Number(serverConfig.timeout);
|
|
143
|
-
if (Number.isFinite(parsed) && parsed
|
|
143
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
144
144
|
timeout = parsed;
|
|
145
145
|
} else {
|
|
146
146
|
logger.warn(`MCP server "${serverName}": invalid timeout "${serverConfig.timeout}", ignoring`);
|
|
@@ -74,7 +74,7 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
74
74
|
if (
|
|
75
75
|
typeof serverConfig.timeout === "number" &&
|
|
76
76
|
Number.isFinite(serverConfig.timeout) &&
|
|
77
|
-
serverConfig.timeout
|
|
77
|
+
serverConfig.timeout >= 0
|
|
78
78
|
) {
|
|
79
79
|
timeout = serverConfig.timeout;
|
|
80
80
|
} else {
|
package/src/edit/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
|
|
|
20
20
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
21
21
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
22
22
|
import type { ToolSession } from "../tools";
|
|
23
|
+
import { truncateForPrompt } from "../tools/approval";
|
|
24
|
+
import { isInternalUrlPath } from "../tools/path-utils";
|
|
23
25
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
24
26
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
25
27
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
@@ -266,7 +268,31 @@ async function executeSinglePathEntries(
|
|
|
266
268
|
};
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
function extractApprovalPath(args: unknown): string {
|
|
272
|
+
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
273
|
+
const targetPath = record.path;
|
|
274
|
+
if (typeof targetPath === "string" && targetPath.length > 0) {
|
|
275
|
+
return targetPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const input = typeof record.input === "string" ? record.input : undefined;
|
|
279
|
+
if (!input) return "(unknown)";
|
|
280
|
+
|
|
281
|
+
const hashlineMatch = /^(?:¶|§|@)([^\s#]+)/m.exec(input);
|
|
282
|
+
if (hashlineMatch?.[1]) return hashlineMatch[1];
|
|
283
|
+
|
|
284
|
+
const applyPatchMatch = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+)$/m.exec(input);
|
|
285
|
+
return applyPatchMatch?.[1]?.trim() || "(unknown)";
|
|
286
|
+
}
|
|
287
|
+
|
|
269
288
|
export class EditTool implements AgentTool<TInput> {
|
|
289
|
+
readonly approval = (args: unknown) => {
|
|
290
|
+
const targetPath = extractApprovalPath(args);
|
|
291
|
+
return targetPath !== "(unknown)" && isInternalUrlPath(targetPath) ? "read" : "write";
|
|
292
|
+
};
|
|
293
|
+
readonly formatApprovalDetails = (args: unknown): string[] => [
|
|
294
|
+
`File: ${truncateForPrompt(extractApprovalPath(args))}`,
|
|
295
|
+
];
|
|
270
296
|
readonly name = "edit";
|
|
271
297
|
readonly label = "Edit";
|
|
272
298
|
readonly loadMode = "essential";
|
package/src/edit/modes/patch.ts
CHANGED
|
@@ -1763,7 +1763,7 @@ export async function executePatchSingle(
|
|
|
1763
1763
|
postEditContent.length === preEditContent.length &&
|
|
1764
1764
|
postEditContent.every((b, i) => b === preEditContent[i]);
|
|
1765
1765
|
if (unchanged) {
|
|
1766
|
-
throw new ToolError(`edit appeared successful but file content did not change on disk: ${
|
|
1766
|
+
throw new ToolError(`edit appeared successful but file content did not change on disk: ${path}`, {
|
|
1767
1767
|
path: resolvedPath,
|
|
1768
1768
|
});
|
|
1769
1769
|
}
|
package/src/edit/streaming.ts
CHANGED
|
@@ -386,14 +386,24 @@ function buildHashlineNaturalOrderPreviews(
|
|
|
386
386
|
case "envelope-begin":
|
|
387
387
|
case "envelope-end":
|
|
388
388
|
case "abort":
|
|
389
|
-
case "op-insert":
|
|
390
|
-
case "op-replace":
|
|
391
389
|
case "op-delete":
|
|
392
390
|
continue;
|
|
393
391
|
case "header":
|
|
394
392
|
currentPath = token.path;
|
|
395
393
|
if (currentPath) ensure(currentPath);
|
|
396
394
|
continue;
|
|
395
|
+
case "op-insert":
|
|
396
|
+
case "op-replace":
|
|
397
|
+
// Inline body on the op line itself (`N↓payload`, `A-B:payload`) is
|
|
398
|
+
// payload content that just happens to share a line with the op
|
|
399
|
+
// header — render it the same as a standalone payload token so
|
|
400
|
+
// the very first character the model types after the sigil shows
|
|
401
|
+
// up in the streaming preview. Without this, the preview is
|
|
402
|
+
// empty until a newline arrives, and the renderer falls back to
|
|
403
|
+
// raw input ("A-B: bla bla bla") instead of "+ bla bla bla".
|
|
404
|
+
if (!currentPath || token.inlineBody === undefined) continue;
|
|
405
|
+
ensure(currentPath).push(`+${token.inlineBody}`);
|
|
406
|
+
continue;
|
|
397
407
|
case "blank":
|
|
398
408
|
if (!currentPath) continue;
|
|
399
409
|
ensure(currentPath).push("+");
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Uses brush-core via native bindings for shell execution.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
-
import {
|
|
7
|
+
import { ExponentialYield } from "@oh-my-pi/pi-agent-core/utils/yield";
|
|
8
|
+
import { executeShell, type MinimizerOptions, Shell, type ShellRunResult } from "@oh-my-pi/pi-natives";
|
|
8
9
|
import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
9
10
|
import { OutputSink } from "../session/streaming-output";
|
|
10
11
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
@@ -196,7 +197,10 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
196
197
|
},
|
|
197
198
|
);
|
|
198
199
|
|
|
199
|
-
const
|
|
200
|
+
const ey = new ExponentialYield();
|
|
201
|
+
const winner = await ey.race<
|
|
202
|
+
{ kind: "result"; result: ShellRunResult } | { kind: "timeout" } | { kind: "abort" }
|
|
203
|
+
>([
|
|
200
204
|
runPromise.then(result => ({ kind: "result" as const, result })),
|
|
201
205
|
timeoutDeferred.promise.then(kind => ({ kind })),
|
|
202
206
|
abortDeferred.promise.then(kind => ({ kind })),
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
|
|
16
16
|
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
|
|
17
|
+
import reviewCustomRequestTemplate from "../../../../prompts/review-custom-request.md" with { type: "text" };
|
|
18
|
+
import reviewHeadlessRequestTemplate from "../../../../prompts/review-headless-request.md" with { type: "text" };
|
|
17
19
|
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
|
|
18
20
|
import * as git from "../../../../utils/git";
|
|
19
21
|
|
|
@@ -225,6 +227,14 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, addi
|
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
function buildCustomReviewPrompt(instructions: string): string {
|
|
231
|
+
return prompt.render(reviewCustomRequestTemplate, { instructions });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildHeadlessReviewPrompt(focus?: string): string {
|
|
235
|
+
return prompt.render(reviewHeadlessRequestTemplate, { focus });
|
|
236
|
+
}
|
|
237
|
+
|
|
228
238
|
export class ReviewCommand implements CustomCommand {
|
|
229
239
|
name = "review";
|
|
230
240
|
description = "Launch interactive code review";
|
|
@@ -233,8 +243,7 @@ export class ReviewCommand implements CustomCommand {
|
|
|
233
243
|
|
|
234
244
|
async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
|
|
235
245
|
if (!ctx.hasUI) {
|
|
236
|
-
|
|
237
|
-
return args.length > 0 ? `${base} Focus: ${args.join(" ")}` : base;
|
|
246
|
+
return buildHeadlessReviewPrompt(args.length > 0 ? args.join(" ") : undefined);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
// Inline args act as additional instructions appended to the generated prompt.
|
|
@@ -379,7 +388,12 @@ export class ReviewCommand implements CustomCommand {
|
|
|
379
388
|
|
|
380
389
|
case 4: {
|
|
381
390
|
// Custom instructions - still uses the old approach since user provides context
|
|
382
|
-
const instructions = await ctx.ui.editor(
|
|
391
|
+
const instructions = await ctx.ui.editor(
|
|
392
|
+
"Enter custom review instructions",
|
|
393
|
+
"Review the following:\n\n",
|
|
394
|
+
undefined,
|
|
395
|
+
{ promptStyle: true },
|
|
396
|
+
);
|
|
383
397
|
if (!instructions?.trim()) return undefined;
|
|
384
398
|
|
|
385
399
|
// For custom, we still try to get current diff for context
|
|
@@ -402,17 +416,7 @@ export class ReviewCommand implements CustomCommand {
|
|
|
402
416
|
);
|
|
403
417
|
}
|
|
404
418
|
|
|
405
|
-
|
|
406
|
-
return `## Code Review Request
|
|
407
|
-
|
|
408
|
-
### Mode
|
|
409
|
-
Custom review instructions
|
|
410
|
-
|
|
411
|
-
### Instructions
|
|
412
|
-
|
|
413
|
-
${instructions}
|
|
414
|
-
|
|
415
|
-
Use the Task tool with \`agent: "reviewer"\` to execute this review.`;
|
|
419
|
+
return buildCustomReviewPrompt(instructions);
|
|
416
420
|
}
|
|
417
421
|
|
|
418
422
|
default:
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Custom tools are TypeScript modules that define additional tools for the agent.
|
|
5
5
|
* They can provide custom rendering for tool calls and results in the TUI.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
AgentToolUpdateCallback,
|
|
10
|
+
ToolApproval,
|
|
11
|
+
ToolApprovalDecision,
|
|
12
|
+
ToolTier,
|
|
13
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
14
|
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
9
15
|
import type { Model, Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
10
16
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -23,7 +29,7 @@ export type CustomToolUIContext = HookUIContext;
|
|
|
23
29
|
// Re-export for backward compatibility
|
|
24
30
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
25
31
|
/** Re-export for custom tools to use in execute signature */
|
|
26
|
-
export type { AgentToolResult, AgentToolUpdateCallback };
|
|
32
|
+
export type { AgentToolResult, AgentToolUpdateCallback, ToolApproval, ToolApprovalDecision, ToolTier };
|
|
27
33
|
|
|
28
34
|
/** Pending action entry consumed by the hidden resolve tool */
|
|
29
35
|
export interface CustomToolPendingAction {
|
|
@@ -80,6 +86,8 @@ export interface CustomToolContext {
|
|
|
80
86
|
abort(): void;
|
|
81
87
|
/** Settings instance for the current session. Prefer over the global singleton. */
|
|
82
88
|
settings?: Settings;
|
|
89
|
+
/** Whether to auto-approve all destructive tool operations (--auto-approve CLI flag) */
|
|
90
|
+
autoApprove?: boolean;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
/** Session event passed to onSession callback */
|
|
@@ -191,6 +199,12 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
|
|
|
191
199
|
mcpServerName?: string;
|
|
192
200
|
/** Original MCP tool name for discovery/search metadata. */
|
|
193
201
|
mcpToolName?: string;
|
|
202
|
+
|
|
203
|
+
/** Capability tier declaration used by approval gates. Omitted means "exec". */
|
|
204
|
+
approval?: ToolApproval;
|
|
205
|
+
|
|
206
|
+
/** Lines appended after the standard approval prompt header. */
|
|
207
|
+
formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
|
|
194
208
|
/**
|
|
195
209
|
* Execute the tool.
|
|
196
210
|
* @param toolCallId - Unique ID for this tool call
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import type { Settings } from "../../config/settings";
|
|
6
7
|
import type { Theme } from "../../modes/theme/theme";
|
|
8
|
+
import { type ApprovalMode, formatApprovalPrompt, requiresApproval } from "../../tools/approval";
|
|
7
9
|
import { applyToolProxy } from "../tool-proxy";
|
|
8
10
|
import type { ExtensionRunner } from "./runner";
|
|
9
11
|
import type { RegisteredTool, ToolCallEventResult } from "./types";
|
|
@@ -108,7 +110,40 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
108
110
|
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
|
|
109
111
|
context?: AgentToolContext,
|
|
110
112
|
) {
|
|
111
|
-
//
|
|
113
|
+
// 1. Check approval policy (before extension handlers).
|
|
114
|
+
// CLI `--auto-approve` / `--yolo` forces yolo mode for the session, but
|
|
115
|
+
// tool-level safety overrides still prompt. User `tools.approval.<tool>`
|
|
116
|
+
// policies are honored in every mode.
|
|
117
|
+
const cliAutoApprove = context?.autoApprove === true;
|
|
118
|
+
const settings: Settings | undefined = context?.settings;
|
|
119
|
+
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
120
|
+
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
|
|
121
|
+
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
122
|
+
const approvalCheck = requiresApproval(this.tool, params, approvalMode, userPolicies);
|
|
123
|
+
|
|
124
|
+
if (approvalCheck.required) {
|
|
125
|
+
// Check if UI is available
|
|
126
|
+
if (!this.runner.hasUI()) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Tool "${this.tool.name}" requires approval but no interactive UI available.\n` +
|
|
129
|
+
`Options:\n` +
|
|
130
|
+
` 1. Set tools.approvalMode: yolo in /settings\n` +
|
|
131
|
+
` 2. Add tools.approval.${this.tool.name}: allow to config\n` +
|
|
132
|
+
` 3. Use an interactive UI to approve the tool call`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const uiContext = this.runner.getUIContext();
|
|
137
|
+
const choice = await uiContext.select(formatApprovalPrompt(this.tool, params, approvalCheck.reason), [
|
|
138
|
+
"Approve",
|
|
139
|
+
"Deny",
|
|
140
|
+
]);
|
|
141
|
+
if (choice !== "Approve") {
|
|
142
|
+
throw new Error(`Tool call denied by user: ${this.tool.name}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Emit tool_call event - extensions can block execution
|
|
112
147
|
if (this.runner.hasHandlers("tool_call")) {
|
|
113
148
|
try {
|
|
114
149
|
const callResult = (await this.runner.emitToolCall({
|
|
@@ -139,9 +139,16 @@ export interface HookUIContext {
|
|
|
139
139
|
* Supports Ctrl+G to open external editor ($VISUAL or $EDITOR).
|
|
140
140
|
* @param title - Title describing what is being edited
|
|
141
141
|
* @param prefill - Optional initial text
|
|
142
|
+
* @param options - Optional dialog controls such as an abort signal
|
|
143
|
+
* @param editorOptions - Optional editor behavior; `promptStyle` makes Enter submit and Shift+Enter insert a newline
|
|
142
144
|
* @returns Edited text, or undefined if cancelled (Escape)
|
|
143
145
|
*/
|
|
144
|
-
editor(
|
|
146
|
+
editor(
|
|
147
|
+
title: string,
|
|
148
|
+
prefill?: string,
|
|
149
|
+
options?: { signal?: AbortSignal },
|
|
150
|
+
editorOptions?: { promptStyle?: boolean },
|
|
151
|
+
): Promise<string | undefined>;
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
154
|
* Get the current theme for styling text with ANSI codes.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -264,6 +264,44 @@ function countMatchingSingleStructuralSuffixBoundary(
|
|
|
264
264
|
return shouldDropSingleStructuralBoundary(replacement, replacement.slice(0, -1), expectedBalance) ? 1 : 0;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Single-line non-structural boundary duplicate detector for replacement
|
|
269
|
+
* groups. Mirrors the same boundary check the pure-insert absorber uses for
|
|
270
|
+
* `ANCHOR↓` (leading) / `ANCHOR↑` (trailing) inserts, but applied to the
|
|
271
|
+
* top/bottom edges of an `A-B:payload` range. Catches mistakes like
|
|
272
|
+
* `103-138:const X = …` where line 102 already reads `const X = …` and the
|
|
273
|
+
* user really meant `103-138!` (delete only).
|
|
274
|
+
*
|
|
275
|
+
* Gated by `options.autoDropPureInsertDuplicates`: the existing 2+-line block
|
|
276
|
+
* absorb already runs unconditionally, and the structural single-line
|
|
277
|
+
* absorber is balance-validated; a non-structural single-line duplicate is
|
|
278
|
+
* ambiguous (could be an intentional `2:foo` over a line that happens to
|
|
279
|
+
* sit next to another `foo`), so we only fire when the user has opted in.
|
|
280
|
+
*/
|
|
281
|
+
function countMatchingSingleNonStructuralPrefixDuplicate(
|
|
282
|
+
fileLines: string[],
|
|
283
|
+
startLine: number,
|
|
284
|
+
replacement: string[],
|
|
285
|
+
): number {
|
|
286
|
+
if (replacement.length === 0 || startLine <= 1) return 0;
|
|
287
|
+
const line = replacement[0];
|
|
288
|
+
if (isStructuralClosingBoundaryLine(line)) return 0;
|
|
289
|
+
if (fileLines[startLine - 2] !== line) return 0;
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function countMatchingSingleNonStructuralSuffixDuplicate(
|
|
294
|
+
fileLines: string[],
|
|
295
|
+
endLine: number,
|
|
296
|
+
replacement: string[],
|
|
297
|
+
): number {
|
|
298
|
+
if (replacement.length === 0 || endLine >= fileLines.length) return 0;
|
|
299
|
+
const line = replacement[replacement.length - 1];
|
|
300
|
+
if (isStructuralClosingBoundaryLine(line)) return 0;
|
|
301
|
+
if (fileLines[endLine] !== line) return 0;
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
267
305
|
function hasExternalTargets(lines: Iterable<number>, externalTargetLines: Set<number>): boolean {
|
|
268
306
|
for (const line of lines) {
|
|
269
307
|
if (externalTargetLines.has(line)) return true;
|
|
@@ -552,12 +590,19 @@ function absorbReplacementBoundaryDuplicates(
|
|
|
552
590
|
const deletedBalance = computeDelimiterBalance(
|
|
553
591
|
group.deletes.map(deleteEdit => fileLines[deleteEdit.anchor.line - 1] ?? ""),
|
|
554
592
|
);
|
|
593
|
+
const optInSingleLineAbsorb = options.autoDropPureInsertDuplicates === true;
|
|
555
594
|
const prefixCount =
|
|
556
595
|
countMatchingPrefixBlock(fileLines, startLine, group.replacement) ||
|
|
557
|
-
countMatchingSingleStructuralPrefixBoundary(fileLines, startLine, group.replacement, deletedBalance)
|
|
596
|
+
countMatchingSingleStructuralPrefixBoundary(fileLines, startLine, group.replacement, deletedBalance) ||
|
|
597
|
+
(optInSingleLineAbsorb
|
|
598
|
+
? countMatchingSingleNonStructuralPrefixDuplicate(fileLines, startLine, group.replacement)
|
|
599
|
+
: 0);
|
|
558
600
|
const suffixCount =
|
|
559
601
|
countMatchingSuffixBlock(fileLines, endLine, group.replacement) ||
|
|
560
|
-
countMatchingSingleStructuralSuffixBoundary(fileLines, endLine, group.replacement, deletedBalance)
|
|
602
|
+
countMatchingSingleStructuralSuffixBoundary(fileLines, endLine, group.replacement, deletedBalance) ||
|
|
603
|
+
(optInSingleLineAbsorb
|
|
604
|
+
? countMatchingSingleNonStructuralSuffixDuplicate(fileLines, endLine, group.replacement)
|
|
605
|
+
: 0);
|
|
561
606
|
const prefixLines = contiguousRange(startLine - prefixCount, prefixCount);
|
|
562
607
|
const suffixLines = contiguousRange(endLine + 1, suffixCount);
|
|
563
608
|
const safePrefixCount = hasExternalTargets(prefixLines, allTargetLines) ? 0 : prefixCount;
|