@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.5
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 +89 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/parse.ts +156 -255
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +50 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +229 -17
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +64 -52
- package/src/internal-urls/artifact-protocol.ts +52 -51
- package/src/internal-urls/docs-index.generated.ts +34 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +50 -7
- package/src/internal-urls/mcp-protocol.ts +3 -8
- package/src/internal-urls/memory-protocol.ts +90 -59
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +40 -23
- package/src/internal-urls/rule-protocol.ts +3 -20
- package/src/internal-urls/skill-protocol.ts +5 -27
- package/src/internal-urls/types.ts +18 -2
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/project-prompt.md +10 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +159 -232
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -34
- package/src/prompts/tools/eval.md +27 -16
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +107 -37
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +3 -9
- package/src/task/executor.ts +23 -7
- package/src/task/index.ts +57 -36
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +30 -50
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +59 -44
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +74 -31
- package/src/tools/search.ts +16 -3
- package/src/tools/todo-write.ts +1 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -119
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
package/src/tools/path-utils.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
|
6
6
|
|
|
7
7
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
8
8
|
const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+)?|raw)$/i;
|
|
9
|
+
const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+)?$/i;
|
|
10
|
+
const FILE_RAW_ONLY_RE = /^raw$/i;
|
|
9
11
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
10
12
|
const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
11
13
|
"agent://",
|
|
@@ -110,7 +112,25 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
110
112
|
const candidate = rawPath.slice(colon + 1);
|
|
111
113
|
if (!FILE_LINE_RANGE_RE.test(candidate)) return { path: rawPath };
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
let basePath = rawPath.slice(0, colon);
|
|
116
|
+
let sel = candidate;
|
|
117
|
+
|
|
118
|
+
// Allow a compound trailing selector: `path:1-50:raw` or `path:raw:1-50`.
|
|
119
|
+
// The two chunks must be one line-range plus one `raw`, in either order.
|
|
120
|
+
const innerColon = basePath.lastIndexOf(":");
|
|
121
|
+
if (innerColon > 0) {
|
|
122
|
+
const innerCandidate = basePath.slice(innerColon + 1);
|
|
123
|
+
const innerIsRaw = FILE_RAW_ONLY_RE.test(innerCandidate);
|
|
124
|
+
const outerIsRaw = FILE_RAW_ONLY_RE.test(candidate);
|
|
125
|
+
const innerIsRange = FILE_LINE_RANGE_ONLY_RE.test(innerCandidate);
|
|
126
|
+
const outerIsRange = FILE_LINE_RANGE_ONLY_RE.test(candidate);
|
|
127
|
+
if ((innerIsRaw && outerIsRange) || (innerIsRange && outerIsRaw)) {
|
|
128
|
+
sel = `${innerCandidate}:${candidate}`;
|
|
129
|
+
basePath = basePath.slice(0, innerColon);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { path: basePath, sel };
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
function assertNotInternalUrl(expanded: string, original: string): void {
|
package/src/tools/read.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getFileReadCache } from "../edit/file-read-cache";
|
|
|
12
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
14
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
15
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
15
16
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
16
17
|
import type { InternalUrl } from "../internal-urls/types";
|
|
17
18
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -431,7 +432,7 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
431
432
|
const readSchema = Type.Object({
|
|
432
433
|
path: Type.String({
|
|
433
434
|
description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
|
|
434
|
-
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com
|
|
435
|
+
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com/:1-40"],
|
|
435
436
|
}),
|
|
436
437
|
});
|
|
437
438
|
|
|
@@ -462,34 +463,67 @@ type ReadParams = ReadToolInput;
|
|
|
462
463
|
type ParsedSelector =
|
|
463
464
|
| { kind: "none" }
|
|
464
465
|
| { kind: "raw" }
|
|
465
|
-
| { kind: "lines"; startLine: number; endLine: number | undefined };
|
|
466
|
+
| { kind: "lines"; startLine: number; endLine: number | undefined; raw?: boolean };
|
|
466
467
|
|
|
467
468
|
const LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
|
|
468
469
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
/** Returns true when the selector requested verbatim/raw output (alone or combined with a range). */
|
|
471
|
+
function isRawSelector(parsed: ParsedSelector): boolean {
|
|
472
|
+
return parsed.kind === "raw" || (parsed.kind === "lines" && parsed.raw === true);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function parseLineRangeChunk(sel: string): { startLine: number; endLine: number | undefined } | null {
|
|
472
476
|
const lineMatch = LINE_RANGE_RE.exec(sel);
|
|
473
|
-
if (lineMatch)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
if (!lineMatch) return null;
|
|
478
|
+
const rawStart = Number.parseInt(lineMatch[1]!, 10);
|
|
479
|
+
if (rawStart < 1) {
|
|
480
|
+
throw new ToolError("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
481
|
+
}
|
|
482
|
+
const sep = lineMatch[2];
|
|
483
|
+
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
484
|
+
let rawEnd: number | undefined;
|
|
485
|
+
if (sep === "+") {
|
|
486
|
+
if (rhs === undefined || rhs < 1) {
|
|
487
|
+
throw new ToolError(`Invalid range ${rawStart}+${rhs ?? 0}: count must be >= 1.`);
|
|
477
488
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
+
rawEnd = rawStart + rhs - 1;
|
|
490
|
+
} else if (sep === "-") {
|
|
491
|
+
if (rhs === undefined || rhs < rawStart) {
|
|
492
|
+
throw new ToolError(`Invalid range ${rawStart}-${rhs ?? 0}: end must be >= start.`);
|
|
493
|
+
}
|
|
494
|
+
rawEnd = rhs;
|
|
495
|
+
}
|
|
496
|
+
return { startLine: rawStart, endLine: rawEnd };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function parseSel(sel: string | undefined): ParsedSelector {
|
|
500
|
+
if (!sel || sel.length === 0) return { kind: "none" };
|
|
501
|
+
|
|
502
|
+
// Compound selector: `1-50:raw` or `raw:1-50`. Split into chunks and accept
|
|
503
|
+
// any combination of one line range and the literal `raw`.
|
|
504
|
+
if (sel.includes(":")) {
|
|
505
|
+
const chunks = sel.split(":");
|
|
506
|
+
if (chunks.length === 2) {
|
|
507
|
+
const [a, b] = chunks as [string, string];
|
|
508
|
+
const aIsRaw = a.toLowerCase() === "raw";
|
|
509
|
+
const bIsRaw = b.toLowerCase() === "raw";
|
|
510
|
+
const rangeChunk = aIsRaw ? b : bIsRaw ? a : null;
|
|
511
|
+
const rawChunk = aIsRaw ? a : bIsRaw ? b : null;
|
|
512
|
+
if (rangeChunk !== null && rawChunk !== null) {
|
|
513
|
+
const range = parseLineRangeChunk(rangeChunk);
|
|
514
|
+
if (range) {
|
|
515
|
+
return { kind: "lines", startLine: range.startLine, endLine: range.endLine, raw: true };
|
|
516
|
+
}
|
|
489
517
|
}
|
|
490
|
-
rawEnd = rhs;
|
|
491
518
|
}
|
|
492
|
-
|
|
519
|
+
// Unrecognized compound — fall through (sqlite/archive/url consume their own colon syntax).
|
|
520
|
+
return { kind: "none" };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (sel.toLowerCase() === "raw") return { kind: "raw" };
|
|
524
|
+
const range = parseLineRangeChunk(sel);
|
|
525
|
+
if (range) {
|
|
526
|
+
return { kind: "lines", startLine: range.startLine, endLine: range.endLine };
|
|
493
527
|
}
|
|
494
528
|
// Unrecognized selectors fall through; sqlite/archive/url readers consume their own colon syntax.
|
|
495
529
|
return { kind: "none" };
|
|
@@ -653,9 +687,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
653
687
|
entityLabel: string;
|
|
654
688
|
ignoreResultLimits?: boolean;
|
|
655
689
|
raw?: boolean;
|
|
690
|
+
immutable?: boolean;
|
|
656
691
|
},
|
|
657
692
|
): AgentToolResult<ReadToolDetails> {
|
|
658
|
-
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw });
|
|
693
|
+
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
659
694
|
const details = options.details ?? {};
|
|
660
695
|
const allLines = text.split("\n");
|
|
661
696
|
const totalLines = allLines.length;
|
|
@@ -1139,6 +1174,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1139
1174
|
details: { ...cached.details },
|
|
1140
1175
|
sourceUrl: cached.details.finalUrl,
|
|
1141
1176
|
entityLabel: "URL output",
|
|
1177
|
+
immutable: true,
|
|
1142
1178
|
});
|
|
1143
1179
|
}
|
|
1144
1180
|
return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
|
|
@@ -1146,11 +1182,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1146
1182
|
|
|
1147
1183
|
// Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
|
|
1148
1184
|
const internalTarget = splitPathAndSel(readPath);
|
|
1149
|
-
const internalRouter =
|
|
1150
|
-
if (internalRouter
|
|
1185
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
1186
|
+
if (internalRouter.canHandle(internalTarget.path)) {
|
|
1151
1187
|
const parsed = parseSel(internalTarget.sel);
|
|
1152
1188
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1153
|
-
return this.#handleInternalUrl(internalTarget.path, offset, limit);
|
|
1189
|
+
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) });
|
|
1154
1190
|
}
|
|
1155
1191
|
|
|
1156
1192
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
@@ -1164,7 +1200,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1164
1200
|
limit,
|
|
1165
1201
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1166
1202
|
signal,
|
|
1167
|
-
{ raw: archiveParsed
|
|
1203
|
+
{ raw: isRawSelector(archiveParsed) },
|
|
1168
1204
|
);
|
|
1169
1205
|
}
|
|
1170
1206
|
|
|
@@ -1293,7 +1329,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1293
1329
|
throw error;
|
|
1294
1330
|
}
|
|
1295
1331
|
}
|
|
1296
|
-
} else if (isNotebookPath(absolutePath) && parsed
|
|
1332
|
+
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1297
1333
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1298
1334
|
return this.#buildInMemoryTextResult(
|
|
1299
1335
|
await readEditableNotebookText(absolutePath, localReadPath),
|
|
@@ -1421,7 +1457,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1421
1457
|
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1422
1458
|
}
|
|
1423
1459
|
|
|
1424
|
-
const isRawMode = parsed
|
|
1460
|
+
const isRawMode = isRawSelector(parsed);
|
|
1425
1461
|
const shouldAddHashLines = !isRawMode && displayMode.hashLines;
|
|
1426
1462
|
const shouldAddLineNumbers = isRawMode ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1427
1463
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
@@ -1510,8 +1546,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1510
1546
|
* Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://).
|
|
1511
1547
|
* Supports pagination via offset/limit but rejects them when query extraction is used.
|
|
1512
1548
|
*/
|
|
1513
|
-
async #handleInternalUrl(
|
|
1514
|
-
|
|
1549
|
+
async #handleInternalUrl(
|
|
1550
|
+
url: string,
|
|
1551
|
+
offset?: number,
|
|
1552
|
+
limit?: number,
|
|
1553
|
+
options?: { raw?: boolean },
|
|
1554
|
+
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1555
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
1515
1556
|
|
|
1516
1557
|
// Check if URL has query extraction (agent:// only).
|
|
1517
1558
|
// Use parseInternalUrl which handles colons in host (namespaced skills).
|
|
@@ -1550,6 +1591,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1550
1591
|
sourceInternal: url,
|
|
1551
1592
|
entityLabel: "resource",
|
|
1552
1593
|
ignoreResultLimits: scheme === "skill",
|
|
1594
|
+
immutable: resource.immutable,
|
|
1595
|
+
raw: options?.raw,
|
|
1553
1596
|
});
|
|
1554
1597
|
}
|
|
1555
1598
|
|
package/src/tools/search.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
9
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
11
12
|
import type { Theme } from "../modes/theme/theme";
|
|
12
13
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
13
14
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -121,7 +122,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
121
122
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
122
123
|
const effectiveMultiline = patternHasNewline;
|
|
123
124
|
|
|
124
|
-
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
125
125
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
126
126
|
let searchPath: string;
|
|
127
127
|
let scopePath: string;
|
|
@@ -132,10 +132,14 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
132
132
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
133
133
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
134
134
|
}
|
|
135
|
-
const internalRouter =
|
|
135
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
136
136
|
const resolvedPathInputs: string[] = [];
|
|
137
|
+
// Absolute filesystem paths whose source is immutable (e.g. artifact://,
|
|
138
|
+
// pi://, skill://). Hashline anchors are suppressed for these on a
|
|
139
|
+
// per-file basis, leaving editable mixed-in files untouched.
|
|
140
|
+
const immutableSourcePaths = new Set<string>();
|
|
137
141
|
for (const rawPath of rawPaths) {
|
|
138
|
-
if (!internalRouter
|
|
142
|
+
if (!internalRouter.canHandle(rawPath)) {
|
|
139
143
|
resolvedPathInputs.push(rawPath);
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
@@ -146,8 +150,13 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
146
150
|
if (!resource.sourcePath) {
|
|
147
151
|
throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
|
|
148
152
|
}
|
|
153
|
+
if (resource.immutable) {
|
|
154
|
+
immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
155
|
+
}
|
|
149
156
|
resolvedPathInputs.push(resource.sourcePath);
|
|
150
157
|
}
|
|
158
|
+
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
159
|
+
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
151
160
|
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
152
161
|
// directory is gone, and only error if every entry is missing. Single
|
|
153
162
|
// missing path keeps the original ENOENT semantics.
|
|
@@ -336,6 +345,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
336
345
|
const modelOut: string[] = [];
|
|
337
346
|
const displayOut: string[] = [];
|
|
338
347
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
348
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
349
|
+
const useHashLines = immutableSourcePaths.has(absoluteFilePath)
|
|
350
|
+
? immutableDisplayMode.hashLines
|
|
351
|
+
: baseDisplayMode.hashLines;
|
|
339
352
|
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
340
353
|
let nextWidth = Math.max(width, String(match.lineNumber).length);
|
|
341
354
|
for (const ctx of match.contextBefore ?? []) {
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -631,7 +631,7 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
631
631
|
for (const task of phase.tasks) {
|
|
632
632
|
if (task.status !== "in_progress" || !task.notes || task.notes.length === 0) continue;
|
|
633
633
|
const bar = uiTheme.fg("dim", uiTheme.tree.vertical);
|
|
634
|
-
const title = uiTheme.fg("dim", chalk.italic(
|
|
634
|
+
const title = uiTheme.fg("dim", chalk.italic(`§ notes — ${task.content}`));
|
|
635
635
|
lines.push("");
|
|
636
636
|
lines.push(` ${title}`);
|
|
637
637
|
for (let j = 0; j < task.notes.length; j++) {
|
|
@@ -21,17 +21,23 @@ export interface FileDisplayModeSession {
|
|
|
21
21
|
/**
|
|
22
22
|
* Computes effective line display mode from session settings/env.
|
|
23
23
|
* Hashline mode takes precedence and implies line-addressed output everywhere.
|
|
24
|
-
* Hashlines are suppressed when the edit tool is not available (e.g. explore agents)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Hashlines are suppressed when the edit tool is not available (e.g. explore agents),
|
|
25
|
+
* when the caller signals a `raw` read, and when the source is `immutable`
|
|
26
|
+
* (e.g. internal URLs like artifact://, agent://, memory:// — there is no edit
|
|
27
|
+
* path that could consume the anchors). Raw output is returned as-is.
|
|
27
28
|
*/
|
|
28
|
-
export function resolveFileDisplayMode(
|
|
29
|
+
export function resolveFileDisplayMode(
|
|
30
|
+
session: FileDisplayModeSession,
|
|
31
|
+
options?: { raw?: boolean; immutable?: boolean },
|
|
32
|
+
): FileDisplayMode {
|
|
29
33
|
const { settings } = session;
|
|
30
34
|
const hasEditTool = session.hasEditTool ?? true;
|
|
31
35
|
const editMode = resolveEditMode(session);
|
|
32
36
|
const usesHashLineAnchors = editMode === "hashline";
|
|
33
37
|
const raw = options?.raw === true;
|
|
34
|
-
const
|
|
38
|
+
const immutable = options?.immutable === true;
|
|
39
|
+
const hashLines =
|
|
40
|
+
!raw && !immutable && hasEditTool && usesHashLineAnchors && settings.get("readHashLines") !== false;
|
|
35
41
|
return {
|
|
36
42
|
hashLines,
|
|
37
43
|
lineNumbers: !raw && (hashLines || settings.get("readLineNumbers") === true),
|
|
@@ -273,7 +273,7 @@ export const handleMastodon: SpecialHandler = async (
|
|
|
273
273
|
md += `### ${formatDate(status.created_at)}\n\n`;
|
|
274
274
|
const content = await htmlToBasicMarkdown(status.content);
|
|
275
275
|
md += `${content}\n\n`;
|
|
276
|
-
md +=
|
|
276
|
+
md += `💬 ${status.replies_count} · 🔁 ${status.reblogs_count} · ⭐ ${status.favourites_count}\n\n`;
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -32,19 +32,19 @@ interface RepologyPackage {
|
|
|
32
32
|
function statusIndicator(status: string): string {
|
|
33
33
|
switch (status) {
|
|
34
34
|
case "newest":
|
|
35
|
-
return "
|
|
35
|
+
return "✅"; // green check
|
|
36
36
|
case "devel":
|
|
37
|
-
return "
|
|
37
|
+
return "🚧"; // construction
|
|
38
38
|
case "unique":
|
|
39
|
-
return "
|
|
39
|
+
return "🔵"; // blue circle
|
|
40
40
|
case "outdated":
|
|
41
|
-
return "
|
|
41
|
+
return "🔴"; // red circle
|
|
42
42
|
case "legacy":
|
|
43
|
-
return "
|
|
43
|
+
return "⚠\uFE0F"; // warning
|
|
44
44
|
case "rolling":
|
|
45
|
-
return "
|
|
45
|
+
return "🔄"; // arrows
|
|
46
46
|
default:
|
|
47
|
-
return "
|
|
47
|
+
return "➖"; // minus
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { AsyncJobManager } from "../async";
|
|
2
|
-
import { formatDuration } from "../tools/render-utils";
|
|
3
|
-
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface JobsProtocolOptions {
|
|
6
|
-
getAsyncJobManager: () => AsyncJobManager | undefined;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function formatJobTime(startTime: number): string {
|
|
10
|
-
return new Date(startTime).toISOString();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatJobDuration(startTime: number): string {
|
|
14
|
-
return formatDuration(Math.max(0, Date.now() - startTime));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeJobId(url: InternalUrl): string {
|
|
18
|
-
const host = url.rawHost || url.hostname;
|
|
19
|
-
const pathname = (url.rawPathname ?? url.pathname).replace(/^\/+/, "").trim();
|
|
20
|
-
if (host && pathname) return `${host}/${pathname}`;
|
|
21
|
-
if (host) return host;
|
|
22
|
-
return pathname;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class JobsProtocolHandler implements ProtocolHandler {
|
|
26
|
-
readonly scheme = "jobs";
|
|
27
|
-
|
|
28
|
-
constructor(private readonly options: JobsProtocolOptions) {}
|
|
29
|
-
|
|
30
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
31
|
-
const manager = this.options.getAsyncJobManager();
|
|
32
|
-
if (!manager) {
|
|
33
|
-
const content =
|
|
34
|
-
"# Jobs\n\nBackground job support is disabled. Enable `async.enabled` or `bash.autoBackground.enabled` to use jobs://.";
|
|
35
|
-
return {
|
|
36
|
-
url: url.href,
|
|
37
|
-
content,
|
|
38
|
-
contentType: "text/markdown",
|
|
39
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const jobId = normalizeJobId(url);
|
|
44
|
-
if (!jobId) {
|
|
45
|
-
return this.#listJobs(url, manager);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return this.#getJob(url, manager, jobId);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#listJobs(url: InternalUrl, manager: AsyncJobManager): InternalResource {
|
|
52
|
-
const jobs = manager.getAllJobs();
|
|
53
|
-
const running = jobs.filter(job => job.status === "running").sort((a, b) => a.startTime - b.startTime);
|
|
54
|
-
const done = jobs.filter(job => job.status !== "running").sort((a, b) => b.startTime - a.startTime);
|
|
55
|
-
const ordered = [...running, ...done];
|
|
56
|
-
|
|
57
|
-
if (ordered.length === 0) {
|
|
58
|
-
const content = "# Jobs\n\nNo background jobs found.";
|
|
59
|
-
return {
|
|
60
|
-
url: url.href,
|
|
61
|
-
content,
|
|
62
|
-
contentType: "text/markdown",
|
|
63
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const lines = ordered.map(job => {
|
|
68
|
-
return `- \`${job.id}\` [${job.type}] **${job.status}** — ${job.label} \n started: ${formatJobTime(job.startTime)} · duration: ${formatJobDuration(job.startTime)}`;
|
|
69
|
-
});
|
|
70
|
-
const content = `# Jobs\n\n${ordered.length} job${ordered.length === 1 ? "" : "s"}\n\n${lines.join("\n")}`;
|
|
71
|
-
return {
|
|
72
|
-
url: url.href,
|
|
73
|
-
content,
|
|
74
|
-
contentType: "text/markdown",
|
|
75
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
#getJob(url: InternalUrl, manager: AsyncJobManager, jobId: string): InternalResource {
|
|
80
|
-
const job = manager.getJob(jobId);
|
|
81
|
-
if (!job) {
|
|
82
|
-
const content = `# Job Not Found\n\n404: No async job found with id \`${jobId}\`.`;
|
|
83
|
-
return {
|
|
84
|
-
url: url.href,
|
|
85
|
-
content,
|
|
86
|
-
contentType: "text/markdown",
|
|
87
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const sections = [
|
|
92
|
-
`# Job ${job.id}`,
|
|
93
|
-
"",
|
|
94
|
-
`- type: ${job.type}`,
|
|
95
|
-
`- status: ${job.status}`,
|
|
96
|
-
`- label: ${job.label}`,
|
|
97
|
-
`- start: ${formatJobTime(job.startTime)}`,
|
|
98
|
-
`- duration: ${formatJobDuration(job.startTime)}`,
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
if (job.status === "completed" && job.resultText) {
|
|
102
|
-
sections.push("", "## Result", "", "```", job.resultText, "```");
|
|
103
|
-
}
|
|
104
|
-
if (job.status === "failed" && job.errorText) {
|
|
105
|
-
sections.push("", "## Error", "", "```", job.errorText, "```");
|
|
106
|
-
}
|
|
107
|
-
if (job.status === "cancelled" && job.errorText) {
|
|
108
|
-
sections.push("", "## Cancellation", "", "```", job.errorText, "```");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const content = sections.join("\n");
|
|
112
|
-
return {
|
|
113
|
-
url: url.href,
|
|
114
|
-
content,
|
|
115
|
-
contentType: "text/markdown",
|
|
116
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
package/src/task/template.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
3
|
-
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
4
|
-
import type { TaskItem } from "./types";
|
|
5
|
-
|
|
6
|
-
interface RenderResult {
|
|
7
|
-
/** Full task text sent to the subagent */
|
|
8
|
-
task: string;
|
|
9
|
-
/** Raw per-task assignment text, without prompt template boilerplate */
|
|
10
|
-
assignment: string;
|
|
11
|
-
id: string;
|
|
12
|
-
description: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Build the full task text from shared context and per-task assignment.
|
|
17
|
-
*
|
|
18
|
-
* If context is provided, it is prepended with a separator.
|
|
19
|
-
*/
|
|
20
|
-
export function renderTemplate(
|
|
21
|
-
context: string | undefined,
|
|
22
|
-
task: TaskItem,
|
|
23
|
-
simpleMode: TaskSimpleMode = "default",
|
|
24
|
-
): RenderResult {
|
|
25
|
-
let { id, description, assignment } = task;
|
|
26
|
-
assignment = assignment.trim();
|
|
27
|
-
const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
28
|
-
context = contextEnabled ? context?.trim() : undefined;
|
|
29
|
-
|
|
30
|
-
if (!context || !assignment) {
|
|
31
|
-
if (simpleMode === "independent" && assignment) {
|
|
32
|
-
return {
|
|
33
|
-
task: prompt.render(subagentUserPromptTemplate, { assignment, independentMode: true }),
|
|
34
|
-
assignment,
|
|
35
|
-
id,
|
|
36
|
-
description,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
return { task: assignment || context!, assignment: assignment || context!, id, description };
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
task: prompt.render(subagentUserPromptTemplate, { context, assignment, independentMode: false }),
|
|
43
|
-
assignment,
|
|
44
|
-
id,
|
|
45
|
-
description,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bash command normalizer - extracts patterns that are better handled natively.
|
|
3
|
-
*
|
|
4
|
-
* Detects and extracts:
|
|
5
|
-
* - `| head -n N` / `| head -N` - extracted to headLines
|
|
6
|
-
* - `| tail -n N` / `| tail -N` - extracted to tailLines
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface NormalizedCommand {
|
|
10
|
-
/** Cleaned command with patterns stripped */
|
|
11
|
-
command: string;
|
|
12
|
-
/** Extracted head line count, if any */
|
|
13
|
-
headLines?: number;
|
|
14
|
-
/** Extracted tail line count, if any */
|
|
15
|
-
tailLines?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Pattern to match trailing pipe to head/tail.
|
|
20
|
-
* Captures: full match, command (head/tail), line count
|
|
21
|
-
*
|
|
22
|
-
* Matches:
|
|
23
|
-
* - `| head -n 50`
|
|
24
|
-
* - `| head -50`
|
|
25
|
-
* - `| tail -n 100`
|
|
26
|
-
* - `| tail -100`
|
|
27
|
-
*
|
|
28
|
-
* Does NOT match head/tail with other flags or without line count.
|
|
29
|
-
*/
|
|
30
|
-
const TRAILING_HEAD_TAIL_PATTERN = /\|\s*(head|tail)\s+(?:-n\s*(\d+)|(-\d+))\s*$/;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Normalize a bash command by stripping patterns better handled natively.
|
|
34
|
-
*
|
|
35
|
-
* Extracts `| head -n N` and `| tail -n N` suffixes into separate fields
|
|
36
|
-
* so they can be applied post-execution without breaking streaming.
|
|
37
|
-
*
|
|
38
|
-
* Strips `2>&1` since we already merge stdout/stderr.
|
|
39
|
-
*/
|
|
40
|
-
export function normalizeBashCommand(command: string): NormalizedCommand {
|
|
41
|
-
let normalized = command;
|
|
42
|
-
let headLines: number | undefined;
|
|
43
|
-
let tailLines: number | undefined;
|
|
44
|
-
|
|
45
|
-
// Extract trailing head/tail
|
|
46
|
-
const match = normalized.match(TRAILING_HEAD_TAIL_PATTERN);
|
|
47
|
-
if (match) {
|
|
48
|
-
const [fullMatch, cmd, nValue, dashValue] = match;
|
|
49
|
-
const lineCount = nValue ? Number.parseInt(nValue, 10) : Number.parseInt(dashValue.slice(1), 10);
|
|
50
|
-
|
|
51
|
-
if (cmd === "head") {
|
|
52
|
-
headLines = lineCount;
|
|
53
|
-
} else {
|
|
54
|
-
tailLines = lineCount;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
normalized = normalized.slice(0, -fullMatch.length);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Preserve internal whitespace (important for heredocs / indentation-sensitive scripts)
|
|
61
|
-
normalized = normalized.trim();
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
command: normalized,
|
|
65
|
-
headLines,
|
|
66
|
-
tailLines,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Apply head/tail limits to output text.
|
|
72
|
-
*
|
|
73
|
-
* If both head and tail are specified, head is applied first (take first N lines),
|
|
74
|
-
* then tail is applied (take last M lines of that).
|
|
75
|
-
*/
|
|
76
|
-
export function applyHeadTail(
|
|
77
|
-
text: string,
|
|
78
|
-
headLines?: number,
|
|
79
|
-
tailLines?: number,
|
|
80
|
-
): { text: string; applied: boolean; headApplied?: number; tailApplied?: number } {
|
|
81
|
-
if (!headLines && !tailLines) {
|
|
82
|
-
return { text, applied: false };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let lines = text.split("\n");
|
|
86
|
-
let headApplied: number | undefined;
|
|
87
|
-
let tailApplied: number | undefined;
|
|
88
|
-
|
|
89
|
-
// Apply head first (keep first N lines)
|
|
90
|
-
if (headLines !== undefined && headLines > 0 && lines.length > headLines) {
|
|
91
|
-
lines = lines.slice(0, headLines);
|
|
92
|
-
headApplied = headLines;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Then apply tail (keep last N lines)
|
|
96
|
-
if (tailLines !== undefined && tailLines > 0 && lines.length > tailLines) {
|
|
97
|
-
lines = lines.slice(-tailLines);
|
|
98
|
-
tailApplied = tailLines;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
text: lines.join("\n"),
|
|
103
|
-
applied: headApplied !== undefined || tailApplied !== undefined,
|
|
104
|
-
headApplied,
|
|
105
|
-
tailApplied,
|
|
106
|
-
};
|
|
107
|
-
}
|