@oh-my-pi/pi-coding-agent 16.3.4 → 16.3.6
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 +36 -0
- package/dist/cli.js +3522 -3485
- package/dist/types/edit/file-snapshot-store.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +0 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/shared-events.d.ts +8 -1
- package/dist/types/hindsight/content.d.ts +4 -0
- package/dist/types/internal-urls/artifact-protocol.d.ts +8 -0
- package/dist/types/internal-urls/types.d.ts +10 -0
- package/dist/types/lsp/config.d.ts +4 -0
- package/dist/types/lsp/edits.d.ts +2 -0
- package/dist/types/mcp/oauth-discovery.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +15 -10
- package/dist/types/modes/components/bash-execution.d.ts +6 -0
- package/dist/types/modes/components/eval-execution.d.ts +6 -0
- package/dist/types/modes/components/login-dialog.d.ts +10 -2
- package/dist/types/modes/components/tool-execution.d.ts +3 -13
- package/dist/types/modes/components/transcript-container.d.ts +26 -29
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +20 -3
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -3
- package/dist/types/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +15 -6
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/subprocess/worker-runtime.d.ts +11 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +0 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -2
- package/dist/types/tools/renderers.d.ts +0 -20
- package/dist/types/tools/ssh.d.ts +0 -2
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/git.d.ts +16 -0
- package/package.json +12 -12
- package/src/cli/auth-broker-cli.ts +13 -2
- package/src/cli/tiny-models-cli.ts +12 -5
- package/src/edit/file-snapshot-store.ts +5 -2
- package/src/edit/renderer.ts +0 -5
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/shared-events.ts +9 -1
- package/src/hindsight/content.ts +31 -0
- package/src/internal-urls/artifact-protocol.ts +97 -53
- package/src/internal-urls/types.ts +10 -0
- package/src/lsp/config.ts +15 -0
- package/src/lsp/edits.ts +28 -7
- package/src/lsp/index.ts +46 -4
- package/src/mcp/oauth-discovery.ts +88 -18
- package/src/mnemopi/state.ts +26 -2
- package/src/modes/components/assistant-message.ts +134 -82
- package/src/modes/components/bash-execution.ts +9 -0
- package/src/modes/components/chat-transcript-builder.ts +8 -4
- package/src/modes/components/eval-execution.ts +9 -0
- package/src/modes/components/login-dialog.ts +16 -2
- package/src/modes/components/mcp-add-wizard.ts +9 -1
- package/src/modes/components/tool-execution.ts +4 -50
- package/src/modes/components/transcript-container.ts +82 -458
- package/src/modes/components/tree-selector.ts +9 -3
- package/src/modes/controllers/command-controller.ts +0 -3
- package/src/modes/controllers/event-controller.ts +74 -14
- package/src/modes/controllers/extension-ui-controller.ts +0 -1
- package/src/modes/controllers/input-controller.ts +4 -10
- package/src/modes/controllers/mcp-command-controller.ts +106 -29
- package/src/modes/controllers/selector-controller.ts +9 -1
- package/src/modes/interactive-mode.ts +12 -8
- package/src/modes/rpc/rpc-client.ts +8 -4
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +13 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +18 -0
- package/src/modes/utils/transcript-render-helpers.ts +40 -13
- package/src/modes/utils/ui-helpers.ts +12 -7
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +148 -1
- package/src/session/session-context.ts +7 -0
- package/src/subprocess/worker-runtime.ts +219 -2
- package/src/task/worktree.ts +28 -6
- package/src/tiny/worker.ts +14 -4
- package/src/tools/bash-skill-urls.ts +3 -3
- package/src/tools/bash.ts +0 -4
- package/src/tools/browser/tab-supervisor.ts +47 -3
- package/src/tools/eval-render.ts +0 -20
- package/src/tools/grep.ts +19 -2
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +261 -21
- package/src/tools/renderers.ts +0 -20
- package/src/tools/ssh.ts +0 -16
- package/src/tools/write.ts +13 -6
- package/src/utils/git.ts +20 -0
- package/src/utils/open.ts +51 -6
package/src/tools/read.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { normalizeToLF } from "../edit/normalize";
|
|
|
37
37
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
38
38
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
39
39
|
import { InternalUrlRouter, resolveLocalUrlToFile } from "../internal-urls";
|
|
40
|
+
import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
|
|
40
41
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
41
42
|
import type { InternalUrl } from "../internal-urls/types";
|
|
42
43
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -151,6 +152,7 @@ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx"
|
|
|
151
152
|
|
|
152
153
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
153
154
|
const MAX_SUMMARY_LINES = 20_000;
|
|
155
|
+
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
154
156
|
/**
|
|
155
157
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
156
158
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -332,6 +334,24 @@ function contiguousLineNumbers(startLine: number, count: number): number[] {
|
|
|
332
334
|
return lines;
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
function lineNumbersFromSpans(spans: readonly { startLine: number; endLine: number }[]): number[] {
|
|
338
|
+
const lines: number[] = [];
|
|
339
|
+
for (const span of spans) {
|
|
340
|
+
for (let line = span.startLine; line <= span.endLine; line++) lines.push(line);
|
|
341
|
+
}
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function recordInMemorySeenLines(
|
|
346
|
+
session: ToolSession,
|
|
347
|
+
absolutePath: string | undefined,
|
|
348
|
+
fullText: string,
|
|
349
|
+
seenLines: readonly number[] | undefined,
|
|
350
|
+
): void {
|
|
351
|
+
if (!absolutePath || !path.isAbsolute(absolutePath) || !seenLines || seenLines.length === 0) return;
|
|
352
|
+
getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalizeToLF(fullText), seenLines);
|
|
353
|
+
}
|
|
354
|
+
|
|
335
355
|
function lineNumbersFromEntries(entries: readonly LineEntry[]): number[] {
|
|
336
356
|
const lines: number[] = [];
|
|
337
357
|
for (const entry of entries) {
|
|
@@ -1317,6 +1337,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1317
1337
|
: undefined;
|
|
1318
1338
|
let emittedHashlineHeader = false;
|
|
1319
1339
|
let seenLines: number[] | undefined;
|
|
1340
|
+
let rawSeenLines: number[] | undefined;
|
|
1320
1341
|
const formatText = (content: string, startNum: number): string => {
|
|
1321
1342
|
const lineCount = countTextLines(content);
|
|
1322
1343
|
details.displayContent = {
|
|
@@ -1380,10 +1401,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1380
1401
|
} else if (truncation.truncated) {
|
|
1381
1402
|
const outputLines = truncation.outputLines ?? countTextLines(truncation.content);
|
|
1382
1403
|
const endLineDisplay = startLineDisplay + Math.max(0, outputLines - 1);
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1404
|
+
if (options.raw === true) {
|
|
1405
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, outputLines);
|
|
1406
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1407
|
+
} else {
|
|
1408
|
+
outputText = formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay);
|
|
1409
|
+
}
|
|
1387
1410
|
details.truncation = truncation;
|
|
1388
1411
|
truncationInfo = {
|
|
1389
1412
|
result: truncation,
|
|
@@ -1393,21 +1416,28 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1393
1416
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
1394
1417
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
1395
1418
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1419
|
+
if (options.raw === true) {
|
|
1420
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, userLimitedLines);
|
|
1421
|
+
outputText = formatText(selectedContent, startLineDisplay);
|
|
1422
|
+
} else {
|
|
1423
|
+
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1424
|
+
}
|
|
1400
1425
|
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
1401
1426
|
} else {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1427
|
+
if (options.raw === true) {
|
|
1428
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, endLine - startLine);
|
|
1429
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1430
|
+
} else {
|
|
1431
|
+
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1432
|
+
}
|
|
1406
1433
|
}
|
|
1407
1434
|
|
|
1408
1435
|
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1409
1436
|
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1410
1437
|
}
|
|
1438
|
+
if (options.raw === true && options.sourcePath && options.immutable !== true && rawSeenLines) {
|
|
1439
|
+
recordInMemorySeenLines(this.session, options.sourcePath, text, rawSeenLines);
|
|
1440
|
+
}
|
|
1411
1441
|
resultBuilder.text(outputText);
|
|
1412
1442
|
if (truncationInfo) {
|
|
1413
1443
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -1501,6 +1531,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1501
1531
|
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1502
1532
|
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1503
1533
|
}
|
|
1534
|
+
if (options.raw === true && options.sourcePath && options.immutable !== true && visibleSpans.length > 0) {
|
|
1535
|
+
recordInMemorySeenLines(this.session, options.sourcePath, text, lineNumbersFromSpans(visibleSpans));
|
|
1536
|
+
}
|
|
1504
1537
|
resultBuilder.text(finalText);
|
|
1505
1538
|
return resultBuilder.done();
|
|
1506
1539
|
}
|
|
@@ -1519,6 +1552,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1519
1552
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1520
1553
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1521
1554
|
signal: AbortSignal | undefined,
|
|
1555
|
+
allowBridge = true,
|
|
1522
1556
|
): Promise<{
|
|
1523
1557
|
outputText: string;
|
|
1524
1558
|
columnTruncated: number;
|
|
@@ -1528,7 +1562,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1528
1562
|
const rawSelector = isRawSelector(parsed);
|
|
1529
1563
|
|
|
1530
1564
|
// ACP bridge first — the editor's in-memory buffer is source of truth.
|
|
1531
|
-
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1565
|
+
const bridgePromise = allowBridge ? this.#routeReadThroughBridge(absolutePath) : undefined;
|
|
1532
1566
|
if (bridgePromise !== undefined) {
|
|
1533
1567
|
try {
|
|
1534
1568
|
const bridgeText = await bridgePromise;
|
|
@@ -1612,14 +1646,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1612
1646
|
}
|
|
1613
1647
|
if (cloned) displayLines = cloned;
|
|
1614
1648
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1649
|
+
if (displayLines.length > 0) {
|
|
1650
|
+
const endLine = range.startLine + displayLines.length - 1;
|
|
1651
|
+
visibleSpans.push({ startLine: range.startLine, endLine });
|
|
1652
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
1653
|
+
displayLineByNumber.set(range.startLine + i, displayLines[i] ?? "");
|
|
1654
|
+
}
|
|
1655
|
+
if (!fullLines || rawSelector) {
|
|
1656
|
+
const blockText = displayLines.join("\n");
|
|
1657
|
+
blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
1658
|
+
}
|
|
1623
1659
|
}
|
|
1624
1660
|
}
|
|
1625
1661
|
|
|
@@ -1659,6 +1695,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1659
1695
|
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText, clippedLines);
|
|
1660
1696
|
outputText = `${formatReadHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1661
1697
|
}
|
|
1698
|
+
} else if (rawSelector && visibleSpans.length > 0) {
|
|
1699
|
+
const rawSeenLines = lineNumbersFromSpans(visibleSpans);
|
|
1700
|
+
if (rawSeenLines.length > 0) await recordFileSnapshot(this.session, absolutePath, rawSeenLines);
|
|
1662
1701
|
}
|
|
1663
1702
|
if (notices.length > 0) {
|
|
1664
1703
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
@@ -2666,6 +2705,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2666
2705
|
if (hashContext?.tag) {
|
|
2667
2706
|
recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText, clippedLines);
|
|
2668
2707
|
}
|
|
2708
|
+
if (rawSelector && !firstLineExceedsLimit && collectedLines.length > 0) {
|
|
2709
|
+
await recordFileSnapshot(
|
|
2710
|
+
this.session,
|
|
2711
|
+
absolutePath,
|
|
2712
|
+
contiguousLineNumbers(startLineDisplay, collectedLines.length),
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2669
2715
|
|
|
2670
2716
|
if (capturedDisplayContent) {
|
|
2671
2717
|
details.displayContent = capturedDisplayContent;
|
|
@@ -2802,6 +2848,197 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2802
2848
|
return toolResult<ReadToolDetails>(details).text(summary).sourcePath(absolutePath).done();
|
|
2803
2849
|
}
|
|
2804
2850
|
|
|
2851
|
+
#formatArtifactWorkflowNotice(artifact: ResolvedArtifactFile, artifactUrl: string): string {
|
|
2852
|
+
const displayPath = shortenPath(artifact.path);
|
|
2853
|
+
return `Artifact storage: ${displayPath} (${formatBytes(artifact.size)}). Use ${artifactUrl}:N-M to page, ${artifactUrl}:raw:N-M for verbatim chunks, and the artifact file path for search/copy workflows.`;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
#formatRawArtifactBlockedNotice(artifact: ResolvedArtifactFile, artifactUrl: string): string {
|
|
2857
|
+
const displayPath = shortenPath(artifact.path);
|
|
2858
|
+
return `Unbounded raw read blocked for ${artifactUrl} (${formatBytes(
|
|
2859
|
+
artifact.size,
|
|
2860
|
+
)}). Reading the whole artifact verbatim can exhaust memory. Use ${artifactUrl}:raw:1-3000 for bounded verbatim chunks, ${artifactUrl}:1-3000 for numbered exploration, and the artifact file path for search/copy workflows: ${displayPath}`;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
async #readArtifactFile(
|
|
2864
|
+
url: InternalUrl,
|
|
2865
|
+
parsedSel: ParsedSelector,
|
|
2866
|
+
signal?: AbortSignal,
|
|
2867
|
+
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2868
|
+
const artifact = await resolveArtifactFile(url, {
|
|
2869
|
+
cwd: this.session.cwd,
|
|
2870
|
+
settings: this.session.settings,
|
|
2871
|
+
signal,
|
|
2872
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2873
|
+
skills: this.session.skills,
|
|
2874
|
+
});
|
|
2875
|
+
const artifactUrl = `artifact://${artifact.id}`;
|
|
2876
|
+
const details: ReadToolDetails = {
|
|
2877
|
+
resolvedPath: artifact.path,
|
|
2878
|
+
contentType: "text/plain",
|
|
2879
|
+
};
|
|
2880
|
+
|
|
2881
|
+
if (parsedSel.kind === "raw" && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
2882
|
+
return toolResult<ReadToolDetails>(details)
|
|
2883
|
+
.text(this.#formatRawArtifactBlockedNotice(artifact, artifactUrl))
|
|
2884
|
+
.sourcePath(artifact.path)
|
|
2885
|
+
.sourceInternal(url.href)
|
|
2886
|
+
.done();
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
const rawSelector = isRawSelector(parsedSel);
|
|
2890
|
+
const displayMode = resolveFileDisplayMode(this.session, { raw: rawSelector, immutable: true });
|
|
2891
|
+
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
2892
|
+
const read = await this.#readLocalFileMultiRange(
|
|
2893
|
+
artifact.path,
|
|
2894
|
+
parsedSel.ranges,
|
|
2895
|
+
artifact.size,
|
|
2896
|
+
parsedSel,
|
|
2897
|
+
displayMode,
|
|
2898
|
+
undefined,
|
|
2899
|
+
signal,
|
|
2900
|
+
false,
|
|
2901
|
+
);
|
|
2902
|
+
if (read.bridgeResult) return read.bridgeResult;
|
|
2903
|
+
if (read.displayContent) details.displayContent = read.displayContent;
|
|
2904
|
+
let text = read.outputText;
|
|
2905
|
+
if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
2906
|
+
text = text
|
|
2907
|
+
? `${text}\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`
|
|
2908
|
+
: this.#formatArtifactWorkflowNotice(artifact, artifactUrl);
|
|
2909
|
+
}
|
|
2910
|
+
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
2911
|
+
.text(text)
|
|
2912
|
+
.sourcePath(artifact.path)
|
|
2913
|
+
.sourceInternal(url.href);
|
|
2914
|
+
if (read.columnTruncated > 0) resultBuilder.limits({ columnMax: read.columnTruncated });
|
|
2915
|
+
return resultBuilder.done();
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const { offset, limit } = selToOffsetLimit(parsedSel);
|
|
2919
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
2920
|
+
const expandStart = offset !== undefined && offset > 1;
|
|
2921
|
+
const expandEnd = limit !== undefined;
|
|
2922
|
+
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0;
|
|
2923
|
+
const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0;
|
|
2924
|
+
const startLine = requestedStart - leadingContext;
|
|
2925
|
+
const startLineDisplay = startLine + 1;
|
|
2926
|
+
const effectiveLimit = limit ?? this.#defaultLimit;
|
|
2927
|
+
const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
2928
|
+
const selectedLineLimit = effectiveLimit + leadingContext + trailingContext;
|
|
2929
|
+
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
2930
|
+
const streamResult = await streamLinesFromFile(
|
|
2931
|
+
artifact.path,
|
|
2932
|
+
startLine,
|
|
2933
|
+
maxLinesToCollect,
|
|
2934
|
+
maxBytesForRead,
|
|
2935
|
+
selectedLineLimit,
|
|
2936
|
+
signal,
|
|
2937
|
+
artifact.size > SNAPSHOT_MAX_BYTES,
|
|
2938
|
+
);
|
|
2939
|
+
const {
|
|
2940
|
+
lines: collectedLines,
|
|
2941
|
+
totalFileLines,
|
|
2942
|
+
collectedBytes,
|
|
2943
|
+
stoppedByByteLimit,
|
|
2944
|
+
firstLinePreview,
|
|
2945
|
+
firstLineByteLength,
|
|
2946
|
+
reachedEof,
|
|
2947
|
+
} = streamResult;
|
|
2948
|
+
|
|
2949
|
+
if (requestedStart >= totalFileLines) {
|
|
2950
|
+
const suggestion =
|
|
2951
|
+
totalFileLines === 0
|
|
2952
|
+
? "The artifact is empty."
|
|
2953
|
+
: `Use ${artifactUrl}:1 to read from the start, or ${artifactUrl}:${totalFileLines} to read the last line.`;
|
|
2954
|
+
return toolResult<ReadToolDetails>(details)
|
|
2955
|
+
.text(`Line ${requestedStart + 1} is beyond end of artifact (${totalFileLines} lines total). ${suggestion}`)
|
|
2956
|
+
.sourcePath(artifact.path)
|
|
2957
|
+
.sourceInternal(url.href)
|
|
2958
|
+
.done();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const shouldAddLineNumbers = rawSelector ? false : displayMode.hashLines ? false : displayMode.lineNumbers;
|
|
2962
|
+
const selectedContent = collectedLines.join("\n");
|
|
2963
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
2964
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
2965
|
+
const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
2966
|
+
const truncation: TruncationResult = {
|
|
2967
|
+
content: selectedContent,
|
|
2968
|
+
truncated: wasTruncated,
|
|
2969
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
2970
|
+
totalLines: totalSelectedLines,
|
|
2971
|
+
totalBytes: collectedBytes,
|
|
2972
|
+
outputLines: collectedLines.length,
|
|
2973
|
+
outputBytes: collectedBytes,
|
|
2974
|
+
lastLinePartial: false,
|
|
2975
|
+
firstLineExceedsLimit,
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2978
|
+
let displayContent: { text: string; startLine: number; lineNumbers?: Array<number | null> } | undefined;
|
|
2979
|
+
const formatText = (text: string, startNum: number): string => {
|
|
2980
|
+
const lineCount = countTextLines(text);
|
|
2981
|
+
displayContent = {
|
|
2982
|
+
text,
|
|
2983
|
+
startLine: startNum,
|
|
2984
|
+
lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
|
|
2985
|
+
};
|
|
2986
|
+
return formatTextWithMode(text, startNum, false, shouldAddLineNumbers);
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2989
|
+
let outputText: string;
|
|
2990
|
+
let truncationInfo:
|
|
2991
|
+
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
2992
|
+
| undefined;
|
|
2993
|
+
if (truncation.firstLineExceedsLimit) {
|
|
2994
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
2995
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
2996
|
+
outputText =
|
|
2997
|
+
snippet.text.length > 0
|
|
2998
|
+
? formatText(snippet.text, startLineDisplay)
|
|
2999
|
+
: `[Line ${startLineDisplay} is ${formatBytes(
|
|
3000
|
+
firstLineBytes,
|
|
3001
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
3002
|
+
truncationInfo = {
|
|
3003
|
+
result: truncation,
|
|
3004
|
+
options: {
|
|
3005
|
+
direction: "head",
|
|
3006
|
+
startLine: startLineDisplay,
|
|
3007
|
+
totalFileLines: reachedEof ? totalFileLines : undefined,
|
|
3008
|
+
},
|
|
3009
|
+
};
|
|
3010
|
+
} else {
|
|
3011
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
3012
|
+
if (truncation.truncated) {
|
|
3013
|
+
truncationInfo = {
|
|
3014
|
+
result: truncation,
|
|
3015
|
+
options: {
|
|
3016
|
+
direction: "head",
|
|
3017
|
+
startLine: startLineDisplay,
|
|
3018
|
+
totalFileLines: reachedEof ? totalFileLines : undefined,
|
|
3019
|
+
},
|
|
3020
|
+
};
|
|
3021
|
+
} else if (startLine + collectedLines.length < totalFileLines || !reachedEof) {
|
|
3022
|
+
const nextOffset = startLine + collectedLines.length + 1;
|
|
3023
|
+
outputText += reachedEof
|
|
3024
|
+
? `\n\n[${totalFileLines - (startLine + collectedLines.length)} more lines in artifact. Use ${artifactUrl}:${nextOffset} to continue]`
|
|
3025
|
+
: `\n\n[More lines in artifact (${formatBytes(artifact.size)} total; not scanned to EOF). Use ${artifactUrl}:${nextOffset} to continue]`;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
3030
|
+
outputText += `\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`;
|
|
3031
|
+
}
|
|
3032
|
+
if (displayContent) details.displayContent = displayContent;
|
|
3033
|
+
if (truncationInfo) details.truncation = truncationInfo.result;
|
|
3034
|
+
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
3035
|
+
.text(outputText)
|
|
3036
|
+
.sourcePath(artifact.path)
|
|
3037
|
+
.sourceInternal(url.href);
|
|
3038
|
+
if (truncationInfo) resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
3039
|
+
return resultBuilder.done();
|
|
3040
|
+
}
|
|
3041
|
+
|
|
2805
3042
|
/**
|
|
2806
3043
|
* Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://).
|
|
2807
3044
|
* Supports pagination via offset/limit but rejects them when query extraction is used.
|
|
@@ -2829,6 +3066,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2829
3066
|
const hasQueryExtraction = queryParam !== null && queryParam !== "";
|
|
2830
3067
|
hasExtraction = hasPathExtraction || hasQueryExtraction;
|
|
2831
3068
|
}
|
|
3069
|
+
if (scheme === "artifact") {
|
|
3070
|
+
return this.#readArtifactFile(urlMeta, parsedSel, signal);
|
|
3071
|
+
}
|
|
2832
3072
|
|
|
2833
3073
|
// local:// files are real on-disk paths. Detect image files and emit a
|
|
2834
3074
|
// decoded image block before the text-only resource contract UTF-8
|
package/src/tools/renderers.ts
CHANGED
|
@@ -43,26 +43,6 @@ export type ToolRenderer = {
|
|
|
43
43
|
mergeCallAndResult?: boolean;
|
|
44
44
|
/** Render without background box, inline in the response flow */
|
|
45
45
|
inline?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Whether pending-call rows are provisional: useful on screen while a tool is
|
|
48
|
-
* streaming, but not durable transcript history. `true` means every pending
|
|
49
|
-
* shape is provisional. `"collapsed"` means only the collapsed pending shape
|
|
50
|
-
* is provisional; expanded rendering is top-anchored/append-shaped enough to
|
|
51
|
-
* let the transcript commit its settled prefix. Absent = the pending preview
|
|
52
|
-
* streams rows the result render preserves.
|
|
53
|
-
*/
|
|
54
|
-
provisionalPendingPreview?: boolean | "collapsed";
|
|
55
|
-
/**
|
|
56
|
-
* Whether the partial-result render is provisional: chrome rows (header
|
|
57
|
-
* glyph, frame state) that change between `options.isPartial === true` and
|
|
58
|
-
* the final result render. When `true`, the block is treated as
|
|
59
|
-
* commit-unstable while a partial result is in flight, so the
|
|
60
|
-
* stable-prefix ratchet in `deriveLiveCommitState` cannot promote the
|
|
61
|
-
* partial chrome to native scrollback only to have the final render strand
|
|
62
|
-
* it above the settled frame. Absent = the partial render is byte-stable
|
|
63
|
-
* with the final render and may commit like any settled stream.
|
|
64
|
-
*/
|
|
65
|
-
provisionalPartialResult?: boolean;
|
|
66
46
|
/**
|
|
67
47
|
* Whether the renderer's pending-call path visibly consumes
|
|
68
48
|
* `options.spinnerFrame`. Used to avoid scheduling repaint ticks for live
|
package/src/tools/ssh.ts
CHANGED
|
@@ -386,22 +386,6 @@ export const sshToolRenderer = {
|
|
|
386
386
|
});
|
|
387
387
|
},
|
|
388
388
|
mergeCallAndResult: true,
|
|
389
|
-
// Pending call preview can re-anchor wholesale when the final result inserts
|
|
390
|
-
// the `Output` section, so no pending SSH rows may commit to native
|
|
391
|
-
// scrollback — even when expanded. The expanded pending shape was previously
|
|
392
|
-
// allowed to commit, which left two visible shapes in native scrollback once
|
|
393
|
-
// the result settled: a stale `⏳ SSH: [host]` header above the final frame,
|
|
394
|
-
// and the pending `╰──╯` footer reused in-place as the new `├── Output ──┤`
|
|
395
|
-
// separator with a fresh footer pushed below it.
|
|
396
|
-
provisionalPendingPreview: true,
|
|
397
|
-
// Partial-result chrome (pending icon and frame state) differs from the
|
|
398
|
-
// final SSH glyph/state, so the block stays commit-unstable while
|
|
399
|
-
// `options.isPartial` holds. Without this, a long-running SSH command's
|
|
400
|
-
// stable pending header would be promoted by the stable-prefix ratchet and
|
|
401
|
-
// committed to native scrollback, then the final render's SSH glyph would
|
|
402
|
-
// land below and strand a duplicate pending header above the final frame
|
|
403
|
-
// ([#3177](https://github.com/can1357/oh-my-pi/issues/3177)).
|
|
404
|
-
provisionalPartialResult: true,
|
|
405
389
|
// Streamed args can initially render the SSH placeholder (`⏳ SSH: […]` /
|
|
406
390
|
// `$ …`), then the first partial result inserts the `Output` section and
|
|
407
391
|
// re-anchors the frame. Force a full repaint at that seam so placeholder rows
|
package/src/tools/write.ts
CHANGED
|
@@ -974,7 +974,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
974
974
|
interface WriteRenderArgs {
|
|
975
975
|
path?: string;
|
|
976
976
|
file_path?: string;
|
|
977
|
-
content?:
|
|
977
|
+
content?: unknown;
|
|
978
978
|
}
|
|
979
979
|
|
|
980
980
|
const WRITE_PREVIEW_LINES = 6;
|
|
@@ -990,8 +990,14 @@ function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
|
|
|
990
990
|
return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
|
|
991
991
|
}
|
|
992
992
|
|
|
993
|
-
function normalizeDisplayText(text:
|
|
994
|
-
|
|
993
|
+
function normalizeDisplayText(text: unknown): string {
|
|
994
|
+
let displayText = "";
|
|
995
|
+
if (typeof text === "string") {
|
|
996
|
+
displayText = text;
|
|
997
|
+
} else if (text !== undefined && text !== null) {
|
|
998
|
+
displayText = String(text);
|
|
999
|
+
}
|
|
1000
|
+
return displayText.replace(/\r/g, "");
|
|
995
1001
|
}
|
|
996
1002
|
|
|
997
1003
|
/**
|
|
@@ -1098,11 +1104,12 @@ export const writeToolRenderer = {
|
|
|
1098
1104
|
},
|
|
1099
1105
|
uiTheme,
|
|
1100
1106
|
);
|
|
1107
|
+
const content = normalizeDisplayText(args.content);
|
|
1101
1108
|
const streamingCache = createRenderedStringCache();
|
|
1102
1109
|
return framedBlock(uiTheme, width => {
|
|
1103
|
-
const body =
|
|
1110
|
+
const body = content
|
|
1104
1111
|
? formatStreamingContent(
|
|
1105
|
-
|
|
1112
|
+
content,
|
|
1106
1113
|
Boolean(options?.expanded),
|
|
1107
1114
|
lang,
|
|
1108
1115
|
uiTheme,
|
|
@@ -1130,7 +1137,7 @@ export const writeToolRenderer = {
|
|
|
1130
1137
|
): Component {
|
|
1131
1138
|
const rawPath = args?.file_path || args?.path || "";
|
|
1132
1139
|
const filePath = shortenPath(rawPath);
|
|
1133
|
-
const fileContent = args?.content
|
|
1140
|
+
const fileContent = normalizeDisplayText(args?.content);
|
|
1134
1141
|
const lang = getLanguageFromPath(rawPath);
|
|
1135
1142
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1136
1143
|
// The header shows the cwd-relative path but links to the absolute path the
|
package/src/utils/git.ts
CHANGED
|
@@ -1726,6 +1726,26 @@ export const cherryPick = Object.assign(
|
|
|
1726
1726
|
async abort(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1727
1727
|
await runEffect(cwd, ["cherry-pick", "--abort"], { signal });
|
|
1728
1728
|
},
|
|
1729
|
+
/**
|
|
1730
|
+
* Skip the current commit of an in-progress cherry-pick sequence and
|
|
1731
|
+
* continue with the rest of the range. Use after {@link isEmptyError}
|
|
1732
|
+
* reports the current attempt collapsed to a no-op — the alternative,
|
|
1733
|
+
* `--abort`, throws away every remaining commit in the range.
|
|
1734
|
+
*/
|
|
1735
|
+
async skip(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1736
|
+
await runEffect(cwd, ["cherry-pick", "--skip"], { signal });
|
|
1737
|
+
},
|
|
1738
|
+
/**
|
|
1739
|
+
* True when a cherry-pick failure was caused by the current commit
|
|
1740
|
+
* being empty against HEAD — either redundant with an already-applied
|
|
1741
|
+
* change, or auto-resolved to HEAD by a 3-way merge. Callers should
|
|
1742
|
+
* `--skip` in this case to advance the sequencer rather than aborting
|
|
1743
|
+
* the whole range: an empty commit is not a merge conflict, and any
|
|
1744
|
+
* later commits in the range still deserve to land.
|
|
1745
|
+
*/
|
|
1746
|
+
isEmptyError(err: unknown): boolean {
|
|
1747
|
+
return err instanceof GitCommandError && /the previous cherry-pick is now empty/i.test(err.result.stderr);
|
|
1748
|
+
},
|
|
1729
1749
|
},
|
|
1730
1750
|
);
|
|
1731
1751
|
|
package/src/utils/open.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as url from "node:url";
|
|
4
|
-
import
|
|
4
|
+
import { $which, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
const URL_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
|
|
|
9
9
|
if (
|
|
10
10
|
process.platform !== "linux" ||
|
|
11
11
|
!(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) ||
|
|
12
|
-
|
|
12
|
+
!$which("wslview")
|
|
13
13
|
) {
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
@@ -31,6 +31,24 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the Windows `rundll32.exe` command used to hand a URL/path to the
|
|
36
|
+
* user's registered protocol handler. Anchoring to `%SystemRoot%\System32`
|
|
37
|
+
* (rather than relying on `rundll32` being on `PATH`) survives environments
|
|
38
|
+
* where the machine `PATH` no longer references `System32` — a common
|
|
39
|
+
* real-world misconfiguration where `System32\Wbem` / `WindowsPowerShell` /
|
|
40
|
+
* `OpenSSH` survive but `System32` itself is dropped. Bare `rundll32` on
|
|
41
|
+
* such boxes throws `Executable not found in $PATH: "rundll32"` from
|
|
42
|
+
* `Bun.spawn` before ShellExecute ever sees the URL.
|
|
43
|
+
*/
|
|
44
|
+
function windowsOpenerCommand(target: string): string[] {
|
|
45
|
+
const systemRoot = process.env.SystemRoot?.trim() || process.env.SYSTEMROOT?.trim() || "C:\\Windows";
|
|
46
|
+
// `path.win32` (not the platform-adaptive `path.join`) keeps Windows path
|
|
47
|
+
// separators when tests run under a POSIX host and matches Windows call
|
|
48
|
+
// conventions on the real target.
|
|
49
|
+
const rundll32 = path.win32.join(systemRoot, "System32", "rundll32.exe");
|
|
50
|
+
return [rundll32, "url.dll,FileProtocolHandler", target];
|
|
51
|
+
}
|
|
34
52
|
/** Open a URL or file path in the default browser/application. Best-effort, never throws. */
|
|
35
53
|
export function openPath(urlOrPath: string): void {
|
|
36
54
|
let cmd: string[];
|
|
@@ -39,7 +57,7 @@ export function openPath(urlOrPath: string): void {
|
|
|
39
57
|
cmd = ["open", urlOrPath];
|
|
40
58
|
break;
|
|
41
59
|
case "win32":
|
|
42
|
-
cmd =
|
|
60
|
+
cmd = windowsOpenerCommand(urlOrPath);
|
|
43
61
|
break;
|
|
44
62
|
default: {
|
|
45
63
|
const wslPath = getExistingWslLocalPath(urlOrPath);
|
|
@@ -47,9 +65,36 @@ export function openPath(urlOrPath: string): void {
|
|
|
47
65
|
break;
|
|
48
66
|
}
|
|
49
67
|
}
|
|
68
|
+
let child: Bun.Subprocess | undefined;
|
|
50
69
|
try {
|
|
51
|
-
Bun.spawn(cmd, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
52
|
-
} catch {
|
|
53
|
-
//
|
|
70
|
+
child = Bun.spawn(cmd, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Spawn threw synchronously (missing binary, denied exec, sandbox
|
|
73
|
+
// restriction, …). Best-effort: log so the failure isn't invisible while
|
|
74
|
+
// still letting the caller advertise a copy-URL fallback.
|
|
75
|
+
logger.warn("Failed to open external URL/path", {
|
|
76
|
+
command: cmd[0],
|
|
77
|
+
target: urlOrPath,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
54
81
|
}
|
|
82
|
+
// Detect delayed failures (exec succeeded but the opener exited non-zero)
|
|
83
|
+
// without blocking the caller. Recording them makes silent misconfigurations
|
|
84
|
+
// (e.g. `xdg-open` present but no MIME handler for `https`) diagnosable from
|
|
85
|
+
// `~/.omp/logs/omp.*.log`.
|
|
86
|
+
child.exited.then(
|
|
87
|
+
exitCode => {
|
|
88
|
+
if (typeof exitCode === "number" && exitCode !== 0) {
|
|
89
|
+
logger.warn("External opener exited with non-zero status", {
|
|
90
|
+
command: cmd[0],
|
|
91
|
+
target: urlOrPath,
|
|
92
|
+
exitCode,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
() => {
|
|
97
|
+
// Ignore — awaiting the subprocess is best-effort telemetry.
|
|
98
|
+
},
|
|
99
|
+
);
|
|
55
100
|
}
|