@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.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 +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +65 -2
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/render.ts +14 -2
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +24 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +4 -15
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +4 -12
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +6 -3
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +9 -22
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +30 -2
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +3 -7
package/src/tools/fetch.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureTool } from "../utils/tools-manager";
|
|
|
18
18
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
19
19
|
import { specialHandlers } from "../web/scrapers";
|
|
20
20
|
import type { RenderResult } from "../web/scrapers/types";
|
|
21
|
-
import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
21
|
+
import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
22
22
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
23
23
|
import { applyListLimit } from "./list-limit";
|
|
24
24
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
@@ -253,19 +253,6 @@ function isInlineImageMimeTypeSupported(mimeType: string): boolean {
|
|
|
253
253
|
return SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(mimeType);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
/**
|
|
257
|
-
* Check if content looks like HTML
|
|
258
|
-
*/
|
|
259
|
-
function looksLikeHtml(content: string): boolean {
|
|
260
|
-
const trimmed = content.trim().toLowerCase();
|
|
261
|
-
return (
|
|
262
|
-
trimmed.startsWith("<!doctype") ||
|
|
263
|
-
trimmed.startsWith("<html") ||
|
|
264
|
-
trimmed.startsWith("<head") ||
|
|
265
|
-
trimmed.startsWith("<body")
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
256
|
/**
|
|
270
257
|
* Try fetching URL with .md appended (llms.txt convention)
|
|
271
258
|
*/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a deduplicating recorder for relative file paths.
|
|
5
|
+
* Preserves insertion order in `list`; subsequent duplicates are ignored.
|
|
6
|
+
*/
|
|
7
|
+
export function createFileRecorder(): {
|
|
8
|
+
record: (relativePath: string) => void;
|
|
9
|
+
list: string[];
|
|
10
|
+
} {
|
|
11
|
+
const seen = new Set<string>();
|
|
12
|
+
const list: string[] = [];
|
|
13
|
+
return {
|
|
14
|
+
record(relativePath: string) {
|
|
15
|
+
if (!seen.has(relativePath)) {
|
|
16
|
+
seen.add(relativePath);
|
|
17
|
+
list.push(relativePath);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
list,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strip a leading slash and, when the search scope is a directory, normalize
|
|
26
|
+
* Windows-style separators. For single-file scopes, fall back to the basename
|
|
27
|
+
* so tool output does not leak absolute paths.
|
|
28
|
+
*/
|
|
29
|
+
export function formatResultPath(filePath: string, isDirectory: boolean): string {
|
|
30
|
+
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
31
|
+
if (isDirectory) {
|
|
32
|
+
return cleanPath.replace(/\\/g, "/");
|
|
33
|
+
}
|
|
34
|
+
return path.basename(cleanPath);
|
|
35
|
+
}
|
package/src/tools/find.ts
CHANGED
|
@@ -29,9 +29,12 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
|
29
29
|
import { toolResult } from "./tool-result";
|
|
30
30
|
|
|
31
31
|
const findSchema = Type.Object({
|
|
32
|
-
pattern: Type.String({
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
pattern: Type.String({
|
|
33
|
+
description:
|
|
34
|
+
"Glob pattern including the search path (no separate path param), e.g. 'src/**/*.ts', 'lib/*.json'. Supports comma-separated lists like 'apps/,packages/,phases/'. Simple patterns like '*.ts' recurse from cwd.",
|
|
35
|
+
}),
|
|
36
|
+
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories", default: true })),
|
|
37
|
+
limit: Type.Optional(Type.Number({ description: "Max results", default: 1000 })),
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
export type FindToolInput = Static<typeof findSchema>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the first 12 hex characters of a commit SHA, or undefined when the
|
|
3
|
+
* input is missing. Shared between GitHub tool argument normalization and the
|
|
4
|
+
* run-watch renderer.
|
|
5
|
+
*/
|
|
6
|
+
export function formatShortSha(value: string | undefined): string | undefined {
|
|
7
|
+
if (!value) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return value.slice(0, 12);
|
|
12
|
+
}
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
GhRunWatchViewDetails,
|
|
10
10
|
GhToolDetails,
|
|
11
11
|
} from "./gh";
|
|
12
|
+
import { formatShortSha } from "./gh-format";
|
|
12
13
|
import {
|
|
13
14
|
formatExpandHint,
|
|
14
15
|
formatStatusIcon,
|
|
@@ -29,14 +30,6 @@ const RUNNING_STATUSES = new Set(["in_progress"]);
|
|
|
29
30
|
const PENDING_STATUSES = new Set(["queued", "requested", "waiting", "pending"]);
|
|
30
31
|
const FALLBACK_WIDTH = 80;
|
|
31
32
|
|
|
32
|
-
function formatShortSha(value: string | undefined): string | undefined {
|
|
33
|
-
if (!value) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return value.slice(0, 12);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
33
|
function getWatchHeader(watch: GhRunWatchViewDetails): string {
|
|
41
34
|
if (watch.mode === "run" && watch.run) {
|
|
42
35
|
if (watch.state === "watching") {
|
package/src/tools/gh.ts
CHANGED
|
@@ -14,6 +14,7 @@ import ghSearchIssuesDescription from "../prompts/tools/gh-search-issues.md" wit
|
|
|
14
14
|
import ghSearchPrsDescription from "../prompts/tools/gh-search-prs.md" with { type: "text" };
|
|
15
15
|
import * as git from "../utils/git";
|
|
16
16
|
import type { ToolSession } from ".";
|
|
17
|
+
import { formatShortSha } from "./gh-format";
|
|
17
18
|
import type { OutputMeta } from "./output-meta";
|
|
18
19
|
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
19
20
|
import { toolResult } from "./tool-result";
|
|
@@ -148,7 +149,7 @@ const ghIssueViewSchema = Type.Object({
|
|
|
148
149
|
repo: Type.Optional(
|
|
149
150
|
Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full issue URL." }),
|
|
150
151
|
),
|
|
151
|
-
comments: Type.Optional(Type.Boolean({ description: "Include issue comments
|
|
152
|
+
comments: Type.Optional(Type.Boolean({ description: "Include issue comments.", default: true })),
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const ghPrViewSchema = Type.Object({
|
|
@@ -161,7 +162,7 @@ const ghPrViewSchema = Type.Object({
|
|
|
161
162
|
repo: Type.Optional(
|
|
162
163
|
Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full pull request URL." }),
|
|
163
164
|
),
|
|
164
|
-
comments: Type.Optional(Type.Boolean({ description: "Include pull request comments
|
|
165
|
+
comments: Type.Optional(Type.Boolean({ description: "Include pull request comments.", default: true })),
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
const ghPrDiffSchema = Type.Object({
|
|
@@ -217,13 +218,13 @@ const ghPrPushSchema = Type.Object({
|
|
|
217
218
|
const ghSearchIssuesSchema = Type.Object({
|
|
218
219
|
query: Type.String({ description: "GitHub issue search query. Supports GitHub search syntax." }),
|
|
219
220
|
repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
|
|
220
|
-
limit: Type.Optional(Type.Number({ description: "Maximum results to return (
|
|
221
|
+
limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
const ghSearchPrsSchema = Type.Object({
|
|
224
225
|
query: Type.String({ description: "GitHub pull request search query. Supports GitHub search syntax." }),
|
|
225
226
|
repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
|
|
226
|
-
limit: Type.Optional(Type.Number({ description: "Maximum results to return (
|
|
227
|
+
limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
|
|
227
228
|
});
|
|
228
229
|
|
|
229
230
|
const ghRunWatchSchema = Type.Object({
|
|
@@ -239,7 +240,7 @@ const ghRunWatchSchema = Type.Object({
|
|
|
239
240
|
}),
|
|
240
241
|
),
|
|
241
242
|
tail: Type.Optional(
|
|
242
|
-
Type.Number({ description: "Number of log lines to include per failed job (
|
|
243
|
+
Type.Number({ description: "Number of log lines to include per failed job (max: 200).", default: 15 }),
|
|
243
244
|
),
|
|
244
245
|
});
|
|
245
246
|
|
|
@@ -545,14 +546,6 @@ function normalizeOptionalString(value: string | null | undefined): string | und
|
|
|
545
546
|
return normalized ? normalized : undefined;
|
|
546
547
|
}
|
|
547
548
|
|
|
548
|
-
function formatShortSha(value: string | undefined): string | undefined {
|
|
549
|
-
if (!value) {
|
|
550
|
-
return undefined;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return value.slice(0, 12);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
549
|
function requireNonEmpty(value: string | null | undefined, label: string): string {
|
|
557
550
|
const normalized = normalizeOptionalString(value);
|
|
558
551
|
if (!normalized) {
|
package/src/tools/grep.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
-
import { computeLineHash } from "../edit/line-hash";
|
|
10
9
|
import { type ChunkedGrepMatch, describeChunkedGrepMatch } from "../edit/modes/chunk";
|
|
11
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
11
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -16,6 +15,8 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
16
15
|
import { resolveEditMode } from "../utils/edit-mode";
|
|
17
16
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
18
17
|
import type { ToolSession } from ".";
|
|
18
|
+
import { createFileRecorder } from "./file-recorder";
|
|
19
|
+
import { formatMatchLine } from "./match-line-format";
|
|
19
20
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
20
21
|
import {
|
|
21
22
|
combineSearchGlobs,
|
|
@@ -34,13 +35,13 @@ const grepSchema = Type.Object({
|
|
|
34
35
|
path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
|
|
35
36
|
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
|
36
37
|
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
|
37
|
-
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search
|
|
38
|
+
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search", default: false })),
|
|
38
39
|
pre: Type.Optional(Type.Number({ description: "Lines of context before matches" })),
|
|
39
40
|
post: Type.Optional(Type.Number({ description: "Lines of context after matches" })),
|
|
40
41
|
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching" })),
|
|
41
|
-
gitignore: Type.Optional(Type.Boolean({ description: "Respect .gitignore files during search
|
|
42
|
-
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches
|
|
43
|
-
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit
|
|
42
|
+
gitignore: Type.Optional(Type.Boolean({ description: "Respect .gitignore files during search", default: true })),
|
|
43
|
+
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches", default: 20 })),
|
|
44
|
+
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit", default: 0 })),
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
export type GrepToolInput = Static<typeof grepSchema>;
|
|
@@ -243,15 +244,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
243
244
|
? roundRobinSelect(result.matches, effectiveLimit)
|
|
244
245
|
: result.matches.slice(0, effectiveLimit);
|
|
245
246
|
const matchLimitReached = result.matches.length > effectiveLimit;
|
|
246
|
-
const
|
|
247
|
-
const fileList: string[] = [];
|
|
247
|
+
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
248
248
|
const fileMatchCounts = new Map<string, number>();
|
|
249
|
-
const recordFile = (relativePath: string) => {
|
|
250
|
-
if (!files.has(relativePath)) {
|
|
251
|
-
files.add(relativePath);
|
|
252
|
-
fileList.push(relativePath);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
249
|
if (selectedMatches.length === 0) {
|
|
256
250
|
const details: GrepToolDetails = {
|
|
257
251
|
scopePath,
|
|
@@ -401,15 +395,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
401
395
|
}
|
|
402
396
|
}
|
|
403
397
|
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
404
|
-
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
|
|
405
|
-
|
|
406
|
-
if (useHashLines) {
|
|
407
|
-
const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
|
|
408
|
-
return `${ref}${separator}${line}`;
|
|
409
|
-
}
|
|
410
|
-
const padded = lineNumber.toString().padStart(lineWidth, " ");
|
|
411
|
-
return `${padded}${separator}${line}`;
|
|
412
|
-
};
|
|
398
|
+
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
|
|
399
|
+
formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
|
|
413
400
|
if (match.contextBefore) {
|
|
414
401
|
for (const ctx of match.contextBefore) {
|
|
415
402
|
outputLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
@@ -197,3 +197,19 @@ function normalizeMixedSchemaNode(schema: unknown): unknown {
|
|
|
197
197
|
export function jtdToJsonSchema(schema: unknown): unknown {
|
|
198
198
|
return normalizeMixedSchemaNode(schema);
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Normalize a schema input that may be a JSON string, object, or null/undefined.
|
|
203
|
+
* Returns { normalized } on success, or { error } if JSON parsing fails.
|
|
204
|
+
*/
|
|
205
|
+
export function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
206
|
+
if (schema === undefined || schema === null) return {};
|
|
207
|
+
if (typeof schema === "string") {
|
|
208
|
+
try {
|
|
209
|
+
return { normalized: JSON.parse(schema) };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { normalized: schema };
|
|
215
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { computeLineHash } from "../edit/line-hash";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a single line of match output for grep/ast-grep style results.
|
|
5
|
+
* Uses hashline refs when hashlines are enabled, otherwise pads the number.
|
|
6
|
+
*/
|
|
7
|
+
export function formatMatchLine(
|
|
8
|
+
lineNumber: number,
|
|
9
|
+
line: string,
|
|
10
|
+
isMatch: boolean,
|
|
11
|
+
options: { useHashLines: boolean; lineWidth: number },
|
|
12
|
+
): string {
|
|
13
|
+
const separator = isMatch ? ":" : "-";
|
|
14
|
+
if (options.useHashLines) {
|
|
15
|
+
const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
|
|
16
|
+
return `${ref}${separator}${line}`;
|
|
17
|
+
}
|
|
18
|
+
const padded = lineNumber.toString().padStart(options.lineWidth, " ");
|
|
19
|
+
return `${padded}${separator}${line}`;
|
|
20
|
+
}
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -74,7 +74,7 @@ function normalizeAtPrefix(filePath: string): string {
|
|
|
74
74
|
withoutAt.startsWith("artifact://") ||
|
|
75
75
|
withoutAt.startsWith("skill://") ||
|
|
76
76
|
withoutAt.startsWith("rule://") ||
|
|
77
|
-
withoutAt.startsWith("local
|
|
77
|
+
withoutAt.startsWith("local:") ||
|
|
78
78
|
withoutAt.startsWith("mcp://")
|
|
79
79
|
) {
|
|
80
80
|
return withoutAt;
|
|
@@ -110,6 +110,29 @@ export function expandPath(filePath: string): string {
|
|
|
110
110
|
return expandTilde(normalized);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function assertNotInternalUrl(expanded: string, original: string): void {
|
|
114
|
+
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
115
|
+
if (expanded.startsWith(prefix)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeLocalScheme(filePath: string): string {
|
|
124
|
+
return filePath.replace(/^(local:)\/(?!\/)/, "$1//");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isInternalUrlPath(filePath: string): boolean {
|
|
128
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
129
|
+
const expandedAndNormalized = normalizeLocalScheme(expandPath(normalized));
|
|
130
|
+
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
131
|
+
if (expandedAndNormalized.startsWith(prefix)) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
/**
|
|
114
137
|
* Resolve a path relative to the given cwd.
|
|
115
138
|
* Handles ~ expansion and absolute paths.
|
|
@@ -119,7 +142,12 @@ export function expandPath(filePath: string): string {
|
|
|
119
142
|
* filesystem root is almost never what they intended.
|
|
120
143
|
*/
|
|
121
144
|
export function resolveToCwd(filePath: string, cwd: string): string {
|
|
122
|
-
const
|
|
145
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
146
|
+
const expanded = expandPath(normalized);
|
|
147
|
+
const expandedAndNormalized = normalizeLocalScheme(expanded);
|
|
148
|
+
|
|
149
|
+
assertNotInternalUrl(expandedAndNormalized, normalized);
|
|
150
|
+
|
|
123
151
|
if (/^\/+$/.test(expanded)) {
|
|
124
152
|
return cwd;
|
|
125
153
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
2
2
|
import type { ToolSession } from ".";
|
|
3
|
-
import { resolveToCwd } from "./path-utils";
|
|
3
|
+
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
4
4
|
import { ToolError } from "./tool-errors";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const LOCAL_SCHEME_PREFIX = "local:";
|
|
7
7
|
|
|
8
8
|
export function resolvePlanPath(session: ToolSession, targetPath: string): string {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const normalized = normalizeLocalScheme(targetPath);
|
|
10
|
+
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
|
|
11
|
+
return resolveLocalUrlToPath(normalized, {
|
|
11
12
|
getArtifactsDir: session.getArtifactsDir,
|
|
12
13
|
getSessionId: session.getSessionId,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
return resolveToCwd(
|
|
17
|
+
return resolveToCwd(normalized, session.cwd);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function enforcePlanModeWrite(
|
package/src/tools/python.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const pythonSchema = Type.Object({
|
|
|
52
52
|
}),
|
|
53
53
|
{ description: "Cells to execute sequentially in persistent kernel" },
|
|
54
54
|
),
|
|
55
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
55
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 30 })),
|
|
56
56
|
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
|
|
57
57
|
reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })),
|
|
58
58
|
});
|
package/src/tools/read.ts
CHANGED
|
@@ -371,7 +371,7 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
371
371
|
const readSchema = Type.Object({
|
|
372
372
|
path: Type.String({ description: "Path or URL to read" }),
|
|
373
373
|
sel: Type.Optional(Type.String({ description: "Selector: chunk path, L10-L50, or raw" })),
|
|
374
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
374
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 20 })),
|
|
375
375
|
});
|
|
376
376
|
|
|
377
377
|
export type ReadToolInput = Static<typeof readSchema>;
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Provides consistent formatting, truncation, and display patterns across all
|
|
5
5
|
* tool renderers to ensure a unified TUI experience.
|
|
6
6
|
*/
|
|
7
|
+
|
|
7
8
|
import * as os from "node:os";
|
|
8
9
|
import * as path from "node:path";
|
|
10
|
+
import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
|
|
9
11
|
import type { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
10
12
|
import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
11
13
|
import { pluralize } from "@oh-my-pi/pi-utils";
|
|
@@ -210,6 +212,10 @@ interface ParsedDiagnostic {
|
|
|
210
212
|
code?: string;
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
function sanitizeDiagnosticDisplayText(text: string): string {
|
|
216
|
+
return replaceTabs(text);
|
|
217
|
+
}
|
|
218
|
+
|
|
213
219
|
function getSeverityRank(severity: ParsedDiagnostic["severity"]): number {
|
|
214
220
|
switch (severity) {
|
|
215
221
|
case "error":
|
|
@@ -227,13 +233,13 @@ function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
|
|
|
227
233
|
const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
228
234
|
if (!match) return null;
|
|
229
235
|
return {
|
|
230
|
-
filePath: match[1],
|
|
236
|
+
filePath: sanitizeDiagnosticDisplayText(match[1]),
|
|
231
237
|
line: parseInt(match[2], 10),
|
|
232
238
|
col: parseInt(match[3], 10),
|
|
233
239
|
severity: match[4] as ParsedDiagnostic["severity"],
|
|
234
|
-
source: match[5],
|
|
235
|
-
message: match[6],
|
|
236
|
-
code: match[7],
|
|
240
|
+
source: match[5] ? sanitizeDiagnosticDisplayText(match[5]) : undefined,
|
|
241
|
+
message: sanitizeDiagnosticDisplayText(match[6]),
|
|
242
|
+
code: match[7] ? sanitizeDiagnosticDisplayText(match[7]) : undefined,
|
|
237
243
|
};
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -255,7 +261,7 @@ export function formatDiagnostics(
|
|
|
255
261
|
existing.push(parsed);
|
|
256
262
|
byFile.set(parsed.filePath, existing);
|
|
257
263
|
} else {
|
|
258
|
-
unparsed.push(msg);
|
|
264
|
+
unparsed.push(sanitizeDiagnosticDisplayText(msg));
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
|
|
@@ -272,7 +278,8 @@ export function formatDiagnostics(
|
|
|
272
278
|
const headerIcon = diag.errored
|
|
273
279
|
? theme.styledSymbol("status.error", "error")
|
|
274
280
|
: theme.styledSymbol("status.warning", "warning");
|
|
275
|
-
|
|
281
|
+
const summary = sanitizeDiagnosticDisplayText(diag.summary);
|
|
282
|
+
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", `(${summary})`)}`;
|
|
276
283
|
|
|
277
284
|
const maxDiags = expanded ? diag.messages.length : 5;
|
|
278
285
|
let diagsShown = 0;
|
|
@@ -616,3 +623,28 @@ export function formatParseErrors(errors: string[]): string[] {
|
|
|
616
623
|
: "Parse issues:";
|
|
617
624
|
return [header, ...capped.map(err => `- ${err}`)];
|
|
618
625
|
}
|
|
626
|
+
|
|
627
|
+
// =============================================================================
|
|
628
|
+
// LSP Batching
|
|
629
|
+
// =============================================================================
|
|
630
|
+
|
|
631
|
+
const LSP_BATCH_TOOLS = new Set(["edit", "write"]);
|
|
632
|
+
|
|
633
|
+
export interface LspBatchRequest {
|
|
634
|
+
id: string;
|
|
635
|
+
flush: boolean;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function getLspBatchRequest(toolCall: ToolCallContext | undefined): LspBatchRequest | undefined {
|
|
639
|
+
if (!toolCall) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
const hasOtherWrites = toolCall.toolCalls.some(
|
|
643
|
+
(call, index) => index !== toolCall.index && LSP_BATCH_TOOLS.has(call.name),
|
|
644
|
+
);
|
|
645
|
+
if (!hasOtherWrites) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some(call => LSP_BATCH_TOOLS.has(call.name));
|
|
649
|
+
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
650
|
+
}
|
package/src/tools/renderers.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
51
51
|
python: pythonToolRenderer as ToolRenderer,
|
|
52
52
|
calc: calculatorToolRenderer as ToolRenderer,
|
|
53
53
|
edit: editToolRenderer as ToolRenderer,
|
|
54
|
+
apply_patch: editToolRenderer as ToolRenderer,
|
|
54
55
|
find: findToolRenderer as ToolRenderer,
|
|
55
56
|
grep: grepToolRenderer as ToolRenderer,
|
|
56
57
|
lsp: lspToolRenderer as ToolRenderer,
|
package/src/tools/ssh.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { loadCapability } from "../discovery";
|
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
|
|
12
|
-
import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
|
|
12
|
+
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
13
13
|
import type { SSHHostInfo } from "../ssh/connection-manager";
|
|
14
14
|
import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
|
|
15
15
|
import { executeSSH } from "../ssh/ssh-executor";
|
|
@@ -25,7 +25,7 @@ const sshSchema = Type.Object({
|
|
|
25
25
|
host: Type.String({ description: "Host name from managed SSH config or discovered ssh.json files" }),
|
|
26
26
|
command: Type.String({ description: "Command to execute on the remote host" }),
|
|
27
27
|
cwd: Type.Optional(Type.String({ description: "Remote working directory (optional)" })),
|
|
28
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
28
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 60 })),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
export interface SSHToolDetails {
|
|
@@ -168,15 +168,7 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
|
168
168
|
compatEnabled: hostInfo.compatEnabled,
|
|
169
169
|
artifactPath,
|
|
170
170
|
artifactId,
|
|
171
|
-
onChunk:
|
|
172
|
-
tailBuffer.append(chunk);
|
|
173
|
-
if (onUpdate) {
|
|
174
|
-
onUpdate({
|
|
175
|
-
content: [{ type: "text", text: tailBuffer.text() }],
|
|
176
|
-
details: {},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
},
|
|
171
|
+
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
180
172
|
});
|
|
181
173
|
|
|
182
174
|
if (result.cancelled) {
|
|
@@ -10,7 +10,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
10
10
|
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
11
11
|
import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
|
|
12
12
|
import type { ToolSession } from ".";
|
|
13
|
-
import { jtdToJsonSchema } from "./jtd-to-json-schema";
|
|
13
|
+
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
|
|
14
14
|
|
|
15
15
|
export interface SubmitResultDetails {
|
|
16
16
|
data: unknown;
|
|
@@ -20,18 +20,6 @@ export interface SubmitResultDetails {
|
|
|
20
20
|
|
|
21
21
|
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
22
22
|
|
|
23
|
-
function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
24
|
-
if (schema === undefined || schema === null) return {};
|
|
25
|
-
if (typeof schema === "string") {
|
|
26
|
-
try {
|
|
27
|
-
return { normalized: JSON.parse(schema) };
|
|
28
|
-
} catch (err) {
|
|
29
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { normalized: schema };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
23
|
function formatSchema(schema: unknown): string {
|
|
36
24
|
if (schema === undefined) return "No schema provided.";
|
|
37
25
|
if (typeof schema === "string") return schema;
|