@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.67
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 +74 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +24 -5
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/modes/components/assistant-message.d.ts +16 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/eval-render.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +25 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +25 -5
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +100 -27
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/__tests__/shared-executors.test.ts +2 -2
- package/src/eval/agent-bridge.ts +4 -5
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/client.ts +80 -2
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/main.ts +8 -2
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +44 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/tool-execution.ts +71 -13
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +67 -12
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +52 -17
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +11 -2
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +43 -14
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/system-prompt.ts +4 -0
- package/src/task/render.ts +38 -11
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-render.ts +24 -9
- package/src/tools/eval.ts +14 -19
- package/src/tools/fetch.ts +34 -14
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +65 -19
- package/src/tools/render-utils.ts +46 -0
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tools/ssh.ts +21 -8
- package/src/tools/write.ts +17 -8
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/hyperlink.ts +42 -7
- package/src/tui/output-block.ts +14 -0
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +23 -55
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
package/src/tools/eval.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { jsBackend, pythonBackend } from "../eval";
|
|
6
6
|
import type { ExecutorBackend, ExecutorBackendResult } from "../eval/backend";
|
|
7
|
-
import {
|
|
7
|
+
import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../eval/bridge-timeout";
|
|
8
8
|
import { IdleTimeout } from "../eval/idle-timeout";
|
|
9
9
|
import { defaultEvalSessionId } from "../eval/session-id";
|
|
10
10
|
import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
@@ -313,16 +313,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
313
313
|
for (let i = 0; i < cells.length; i++) {
|
|
314
314
|
const cell = cells[i];
|
|
315
315
|
const backend = cell.resolved.backend;
|
|
316
|
-
// The per-cell `timeout` is a
|
|
317
|
-
// work
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// agent/llm is bounded by a plain wall-clock timeout. The watchdog
|
|
324
|
-
// drives `combinedSignal`; we pass no wall-clock deadline downstream
|
|
325
|
-
// so the backends never arm a competing fixed timer.
|
|
316
|
+
// The per-cell `timeout` is a budget on the cell runtime's *own*
|
|
317
|
+
// work. Host-side `agent()`/`parallel()`/`llm()` bridge calls suspend
|
|
318
|
+
// that budget entirely and restart a fresh timeout window when control
|
|
319
|
+
// returns to Python/JS. Compute, stdout, `log()`/`phase()`, and
|
|
320
|
+
// ordinary tool calls all count against the budget. The watchdog drives
|
|
321
|
+
// `combinedSignal`; we pass no wall-clock deadline downstream so the
|
|
322
|
+
// backends never arm a competing fixed timer.
|
|
326
323
|
const idleTimeoutMs = timeoutSecondsFromMs(cell.timeoutMs) * 1000;
|
|
327
324
|
const idle = new IdleTimeout(idleTimeoutMs);
|
|
328
325
|
const combinedSignal = signal
|
|
@@ -355,14 +352,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
355
352
|
outputSink!.push(chunk);
|
|
356
353
|
},
|
|
357
354
|
onStatus: event => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (event.op === EVAL_HEARTBEAT_OP) {
|
|
365
|
-
idle.bump();
|
|
355
|
+
if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
|
|
356
|
+
idle.pause();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (event.op === EVAL_TIMEOUT_RESUME_OP) {
|
|
360
|
+
idle.resume();
|
|
366
361
|
return;
|
|
367
362
|
}
|
|
368
363
|
cellResult.statusEvents ??= [];
|
package/src/tools/fetch.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { type Theme, theme } from "../modes/theme/theme";
|
|
|
13
13
|
import type { ToolSession } from "../sdk";
|
|
14
14
|
import type { AgentStorage } from "../session/agent-storage";
|
|
15
15
|
import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
|
|
16
|
-
import { renderStatusLine } from "../tui";
|
|
17
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
|
+
import { renderStatusLine, urlHyperlink } from "../tui";
|
|
17
|
+
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
18
18
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
19
19
|
import { ensureTool } from "../utils/tools-manager";
|
|
20
20
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
@@ -1437,6 +1437,27 @@ function countNonEmptyLines(text: string): number {
|
|
|
1437
1437
|
return text.split("\n").filter(l => l.trim()).length;
|
|
1438
1438
|
}
|
|
1439
1439
|
|
|
1440
|
+
function readUrlLinkTarget(input: string): string {
|
|
1441
|
+
try {
|
|
1442
|
+
return parseReadUrlTarget(input)?.path ?? input;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return input;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function formatReadUrlDescription(input: string): string {
|
|
1449
|
+
const target = readUrlLinkTarget(input);
|
|
1450
|
+
const displayUrl = target.match(/^www\./i) ? `https://${target}` : target;
|
|
1451
|
+
const domain = getDomain(displayUrl);
|
|
1452
|
+
const urlPath = truncate(displayUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1453
|
+
const label = `${domain}${urlPath ? ` ${urlPath}` : ""}`.trim();
|
|
1454
|
+
return urlHyperlink(target, label);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function formatReadUrlMetadataValue(url: string, uiTheme: Theme): string {
|
|
1458
|
+
return urlHyperlink(url, uiTheme.fg("mdLinkUrl", url));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1440
1461
|
/** Render URL read call (URL preview) */
|
|
1441
1462
|
export function renderReadUrlCall(
|
|
1442
1463
|
args: { path?: string; url?: string; raw?: boolean },
|
|
@@ -1444,9 +1465,7 @@ export function renderReadUrlCall(
|
|
|
1444
1465
|
uiTheme: Theme = theme,
|
|
1445
1466
|
): Component {
|
|
1446
1467
|
const url = args.path ?? args.url ?? "";
|
|
1447
|
-
const
|
|
1448
|
-
const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1449
|
-
const description = `${domain}${path ? ` ${path}` : ""}`.trim();
|
|
1468
|
+
const description = formatReadUrlDescription(url);
|
|
1450
1469
|
const meta: string[] = [];
|
|
1451
1470
|
if (args.raw) meta.push("raw");
|
|
1452
1471
|
const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
|
|
@@ -1465,19 +1484,18 @@ export function renderReadUrlResult(
|
|
|
1465
1484
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1466
1485
|
const errorText = (rawErrorText || "No response data").replace(/^Error:\s*/, "");
|
|
1467
1486
|
const urlText = details?.finalUrl ?? details?.url ?? "";
|
|
1468
|
-
const description = urlText ?
|
|
1487
|
+
const description = urlText ? formatReadUrlDescription(urlText) : undefined;
|
|
1469
1488
|
const header = renderStatusLine({ icon: "error", title: "Read", description }, uiTheme);
|
|
1470
1489
|
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
1471
1490
|
const outputBlock = new CachedOutputBlock();
|
|
1472
|
-
return {
|
|
1491
|
+
return markFramedBlockComponent({
|
|
1473
1492
|
render: (width: number) =>
|
|
1474
1493
|
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
1475
1494
|
invalidate: () => outputBlock.invalidate(),
|
|
1476
|
-
};
|
|
1495
|
+
});
|
|
1477
1496
|
}
|
|
1478
1497
|
|
|
1479
|
-
const
|
|
1480
|
-
const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1498
|
+
const description = formatReadUrlDescription(details.finalUrl);
|
|
1481
1499
|
const hasRedirect = details.url !== details.finalUrl;
|
|
1482
1500
|
const hasNotes = details.notes.length > 0;
|
|
1483
1501
|
const truncation = details.meta?.truncation;
|
|
@@ -1487,7 +1505,7 @@ export function renderReadUrlResult(
|
|
|
1487
1505
|
{
|
|
1488
1506
|
icon: truncated ? "warning" : "success",
|
|
1489
1507
|
title: "Read",
|
|
1490
|
-
description
|
|
1508
|
+
description,
|
|
1491
1509
|
},
|
|
1492
1510
|
uiTheme,
|
|
1493
1511
|
);
|
|
@@ -1505,7 +1523,9 @@ export function renderReadUrlResult(
|
|
|
1505
1523
|
`${uiTheme.fg("muted", "Method:")} ${details.method}`,
|
|
1506
1524
|
];
|
|
1507
1525
|
if (hasRedirect) {
|
|
1508
|
-
metadataLines.push(
|
|
1526
|
+
metadataLines.push(
|
|
1527
|
+
`${uiTheme.fg("muted", "Final URL:")} ${formatReadUrlMetadataValue(details.finalUrl, uiTheme)}`,
|
|
1528
|
+
);
|
|
1509
1529
|
}
|
|
1510
1530
|
const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
1511
1531
|
metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
|
|
@@ -1522,7 +1542,7 @@ export function renderReadUrlResult(
|
|
|
1522
1542
|
let lastExpanded: boolean | undefined;
|
|
1523
1543
|
let contentPreviewLines: string[] | undefined;
|
|
1524
1544
|
|
|
1525
|
-
return {
|
|
1545
|
+
return markFramedBlockComponent({
|
|
1526
1546
|
render: (width: number) => {
|
|
1527
1547
|
const { expanded } = options;
|
|
1528
1548
|
|
|
@@ -1562,5 +1582,5 @@ export function renderReadUrlResult(
|
|
|
1562
1582
|
contentPreviewLines = undefined;
|
|
1563
1583
|
lastExpanded = undefined;
|
|
1564
1584
|
},
|
|
1565
|
-
};
|
|
1585
|
+
});
|
|
1566
1586
|
}
|
package/src/tools/gh.ts
CHANGED
|
@@ -792,6 +792,18 @@ function repoFromRepositoryUrl(value: string | undefined): string | undefined {
|
|
|
792
792
|
return value.slice(REPO_API_URL_PREFIX.length);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
function githubRepoSlugEquals(left: string | undefined, right: string): boolean {
|
|
796
|
+
if (left === undefined || left.length !== right.length) return false;
|
|
797
|
+
for (let idx = 0; idx < left.length; idx += 1) {
|
|
798
|
+
let leftCode = left.charCodeAt(idx);
|
|
799
|
+
let rightCode = right.charCodeAt(idx);
|
|
800
|
+
if (leftCode >= 65 && leftCode <= 90) leftCode += 32;
|
|
801
|
+
if (rightCode >= 65 && rightCode <= 90) rightCode += 32;
|
|
802
|
+
if (leftCode !== rightCode) return false;
|
|
803
|
+
}
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
|
|
795
807
|
function apiUserToGhUser(user: GhApiUser | null | undefined): GhUser | undefined {
|
|
796
808
|
if (!user) return undefined;
|
|
797
809
|
const login = user.login ?? undefined;
|
|
@@ -1646,7 +1658,7 @@ async function resolveGitHubRepo(
|
|
|
1646
1658
|
runRepo: string | undefined,
|
|
1647
1659
|
signal?: AbortSignal,
|
|
1648
1660
|
): Promise<string> {
|
|
1649
|
-
if (repo && runRepo && repo
|
|
1661
|
+
if (repo && runRepo && !githubRepoSlugEquals(repo, runRepo)) {
|
|
1650
1662
|
throw new ToolError("run URL repository does not match the provided repo");
|
|
1651
1663
|
}
|
|
1652
1664
|
|
|
@@ -1712,6 +1724,33 @@ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSign
|
|
|
1712
1724
|
return untilAborted(signal, pending);
|
|
1713
1725
|
}
|
|
1714
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Best-effort cached cwd → `owner/repo` resolution that swallows any failure
|
|
1729
|
+
* (not a git checkout, no GitHub remote, `gh` unauthenticated, …) into
|
|
1730
|
+
* `undefined`. Use where the cwd repo is a convenience fallback, not a safety
|
|
1731
|
+
* check.
|
|
1732
|
+
*/
|
|
1733
|
+
async function tryResolveCurrentRepo(cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
|
|
1734
|
+
try {
|
|
1735
|
+
return await resolveDefaultRepoMemoized(cwd, signal);
|
|
1736
|
+
} catch {
|
|
1737
|
+
return undefined;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Best-effort fresh cwd → `owner/repo` resolution for safety checks that must
|
|
1743
|
+
* reflect the repository currently mounted at `cwd`, not the process-lifetime
|
|
1744
|
+
* default-repo cache.
|
|
1745
|
+
*/
|
|
1746
|
+
async function tryResolveCurrentRepoFresh(cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
|
|
1747
|
+
try {
|
|
1748
|
+
return await resolveGitHubRepo(cwd, undefined, undefined, signal);
|
|
1749
|
+
} catch {
|
|
1750
|
+
return undefined;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1715
1754
|
/**
|
|
1716
1755
|
* Matches search-query qualifiers that already scope to a repository, org, or
|
|
1717
1756
|
* user. When present, callers should avoid layering a default `repo:<current>`
|
|
@@ -1738,11 +1777,7 @@ async function resolveSearchRepoScope(
|
|
|
1738
1777
|
): Promise<string | undefined> {
|
|
1739
1778
|
if (repo) return repo;
|
|
1740
1779
|
if (query && REPO_SCOPE_QUALIFIER_PATTERN.test(query)) return undefined;
|
|
1741
|
-
|
|
1742
|
-
return await resolveDefaultRepoMemoized(cwd, signal);
|
|
1743
|
-
} catch {
|
|
1744
|
-
return undefined;
|
|
1745
|
-
}
|
|
1780
|
+
return tryResolveCurrentRepo(cwd, signal);
|
|
1746
1781
|
}
|
|
1747
1782
|
|
|
1748
1783
|
async function resolveGitHubBranchHead(
|
|
@@ -3338,8 +3373,9 @@ async function executeRunWatch(
|
|
|
3338
3373
|
onUpdate: AgentToolUpdateCallback<GhToolDetails> | undefined,
|
|
3339
3374
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3340
3375
|
const branchInput = normalizeOptionalString(params.branch);
|
|
3376
|
+
const explicitRepo = normalizeOptionalString(params.repo);
|
|
3341
3377
|
const runReference = parseRunReference(params.run);
|
|
3342
|
-
const repo = await resolveGitHubRepo(session.cwd,
|
|
3378
|
+
const repo = await resolveGitHubRepo(session.cwd, explicitRepo, runReference.repo, signal);
|
|
3343
3379
|
const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
|
|
3344
3380
|
const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
|
|
3345
3381
|
const tail = resolveTailLimit(params.tail);
|
|
@@ -3419,10 +3455,28 @@ async function executeRunWatch(
|
|
|
3419
3455
|
}
|
|
3420
3456
|
}
|
|
3421
3457
|
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3458
|
+
let branch: string;
|
|
3459
|
+
let headSha: string;
|
|
3460
|
+
if (branchInput) {
|
|
3461
|
+
branch = branchInput;
|
|
3462
|
+
headSha = await resolveGitHubBranchHead(session.cwd, repo, branch, signal);
|
|
3463
|
+
} else {
|
|
3464
|
+
// No branch/run selector — derive the commit from the current checkout,
|
|
3465
|
+
// but only when cwd actually points at `repo`. Otherwise we'd watch an
|
|
3466
|
+
// unrelated commit SHA against the explicit repo and silently stream a
|
|
3467
|
+
// confident wrong-repo status (issue #1949). GitHub `owner/repo` slugs
|
|
3468
|
+
// are case-insensitive — `gh repo view` returns the canonical casing
|
|
3469
|
+
// while callers may pass any casing — so the equality check normalizes
|
|
3470
|
+
// both sides before deciding the cwd is a different repo (PR #1951).
|
|
3471
|
+
const cwdRepo = await tryResolveCurrentRepoFresh(session.cwd, signal);
|
|
3472
|
+
if (!githubRepoSlugEquals(cwdRepo, repo)) {
|
|
3473
|
+
throw new ToolError(
|
|
3474
|
+
`Cannot infer the watched commit for ${repo}: current checkout is ${cwdRepo ?? "not a GitHub repository"}. Pass \`branch\` or \`run\` to scope the watch.`,
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
branch = await requireCurrentGitBranch(session.cwd, signal);
|
|
3478
|
+
headSha = await requireCurrentGitHead(session.cwd, signal);
|
|
3479
|
+
}
|
|
3426
3480
|
let pollCount = 0;
|
|
3427
3481
|
let settledSuccessSignature: string | undefined;
|
|
3428
3482
|
|
package/src/tools/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { CustomMessage } from "../session/messages";
|
|
|
23
23
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
24
24
|
import { TaskTool } from "../task";
|
|
25
25
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
26
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
26
27
|
import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
|
|
27
28
|
import type { EventBus } from "../utils/event-bus";
|
|
28
29
|
import { WebSearchTool } from "../web/search";
|
|
@@ -420,14 +421,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
// Resolve effective tool discovery mode.
|
|
423
|
-
// tools.discoveryMode
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
: session.settings.get("mcp.discoveryMode")
|
|
429
|
-
? "mcp-only"
|
|
430
|
-
: "off";
|
|
424
|
+
// tools.discoveryMode controls the new modes; mcp.discoveryMode remains a back-compat alias for "mcp-only".
|
|
425
|
+
const effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
426
|
+
session.settings,
|
|
427
|
+
countToolsForAutoDiscovery(requestedTools ?? Object.keys(BUILTIN_TOOLS)),
|
|
428
|
+
);
|
|
431
429
|
const discoveryActive = effectiveDiscoveryMode !== "off";
|
|
432
430
|
|
|
433
431
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
package/src/tools/read.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
truncateLine,
|
|
30
30
|
} from "../session/streaming-output";
|
|
31
31
|
import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
|
|
32
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
32
|
+
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
33
33
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
34
34
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
35
35
|
import { convertFileWithMarkit } from "../utils/markit";
|
|
@@ -2316,6 +2316,49 @@ interface ReadRenderArgs {
|
|
|
2316
2316
|
raw?: boolean;
|
|
2317
2317
|
}
|
|
2318
2318
|
|
|
2319
|
+
const INTERNAL_URL_LIKE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
2320
|
+
|
|
2321
|
+
function splitReadRenderPath(rawPath: string): { path: string; sel?: string } {
|
|
2322
|
+
if (INTERNAL_URL_LIKE_RE.test(rawPath)) {
|
|
2323
|
+
const internal = splitInternalUrlSel(rawPath);
|
|
2324
|
+
if (internal.sel) return internal;
|
|
2325
|
+
}
|
|
2326
|
+
return splitPathAndSel(rawPath);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function firstReadSelectorLine(sel: string | undefined): number | undefined {
|
|
2330
|
+
if (!sel) return undefined;
|
|
2331
|
+
try {
|
|
2332
|
+
const parsed = parseSel(sel);
|
|
2333
|
+
if (parsed.kind !== "lines") return undefined;
|
|
2334
|
+
return parsed.ranges[0].startLine;
|
|
2335
|
+
} catch {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function formatReadPathLink(
|
|
2341
|
+
rawPath: string,
|
|
2342
|
+
options: {
|
|
2343
|
+
resolvedPath?: string;
|
|
2344
|
+
suffixResolution?: { from: string; to: string };
|
|
2345
|
+
offset?: number;
|
|
2346
|
+
fallbackLabel?: string;
|
|
2347
|
+
},
|
|
2348
|
+
): string {
|
|
2349
|
+
const split = splitReadRenderPath(rawPath);
|
|
2350
|
+
const basePath = split.path || rawPath;
|
|
2351
|
+
const selectorSuffix = split.sel ? `:${split.sel}` : "";
|
|
2352
|
+
const plainDisplayPath = options.suffixResolution
|
|
2353
|
+
? shortenPath(options.suffixResolution.to)
|
|
2354
|
+
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2355
|
+
const target = options.resolvedPath ?? tryResolveInternalUrlSync(basePath);
|
|
2356
|
+
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2357
|
+
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2358
|
+
const displayPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
2359
|
+
return `${displayPath}${selectorSuffix}`;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2319
2362
|
export const readToolRenderer = {
|
|
2320
2363
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
2321
2364
|
if (isReadableUrlPath(args.file_path || args.path || "")) {
|
|
@@ -2323,13 +2366,10 @@ export const readToolRenderer = {
|
|
|
2323
2366
|
}
|
|
2324
2367
|
|
|
2325
2368
|
const rawPath = args.file_path || args.path || "";
|
|
2326
|
-
const shortPath = shortenPath(rawPath);
|
|
2327
|
-
const linkTarget = tryResolveInternalUrlSync(rawPath);
|
|
2328
|
-
const filePath = linkTarget ? fileHyperlink(linkTarget, shortPath) : shortPath;
|
|
2329
2369
|
const offset = args.offset;
|
|
2330
2370
|
const limit = args.limit;
|
|
2331
2371
|
|
|
2332
|
-
let pathDisplay =
|
|
2372
|
+
let pathDisplay = formatReadPathLink(rawPath, { offset, fallbackLabel: "…" }) || "…";
|
|
2333
2373
|
if (offset !== undefined || limit !== undefined) {
|
|
2334
2374
|
const startLine = offset ?? 1;
|
|
2335
2375
|
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
@@ -2363,7 +2403,7 @@ export const readToolRenderer = {
|
|
|
2363
2403
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
2364
2404
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
2365
2405
|
const rawPath = args?.file_path || args?.path || "";
|
|
2366
|
-
const filePath = shortenPath(rawPath);
|
|
2406
|
+
const filePath = formatReadPathLink(rawPath, { offset: args?.offset }) || shortenPath(rawPath);
|
|
2367
2407
|
let title = filePath ? `Read ${filePath}` : "Read";
|
|
2368
2408
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
2369
2409
|
const startLine = args.offset ?? 1;
|
|
@@ -2373,11 +2413,11 @@ export const readToolRenderer = {
|
|
|
2373
2413
|
const header = renderStatusLine({ icon: "error", title }, uiTheme);
|
|
2374
2414
|
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
2375
2415
|
const outputBlock = new CachedOutputBlock();
|
|
2376
|
-
return {
|
|
2416
|
+
return markFramedBlockComponent({
|
|
2377
2417
|
render: (width: number) =>
|
|
2378
2418
|
outputBlock.render({ header, state: "error", sections: [{ lines: errorLines }], width }, uiTheme),
|
|
2379
2419
|
invalidate: () => outputBlock.invalidate(),
|
|
2380
|
-
};
|
|
2420
|
+
});
|
|
2381
2421
|
}
|
|
2382
2422
|
const details = result.details;
|
|
2383
2423
|
const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
@@ -2388,8 +2428,8 @@ export const readToolRenderer = {
|
|
|
2388
2428
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
2389
2429
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
2390
2430
|
const rawPath = args?.file_path || args?.path || "";
|
|
2391
|
-
const
|
|
2392
|
-
const lang = getLanguageFromPath(
|
|
2431
|
+
const renderPath = splitReadRenderPath(rawPath);
|
|
2432
|
+
const lang = getLanguageFromPath(renderPath.path);
|
|
2393
2433
|
|
|
2394
2434
|
const warningLines: string[] = [];
|
|
2395
2435
|
const truncation = details?.meta?.truncation;
|
|
@@ -2412,7 +2452,11 @@ export const readToolRenderer = {
|
|
|
2412
2452
|
|
|
2413
2453
|
if (imageContent) {
|
|
2414
2454
|
const suffix = details?.suffixResolution;
|
|
2415
|
-
const displayPath =
|
|
2455
|
+
const displayPath = formatReadPathLink(rawPath, {
|
|
2456
|
+
resolvedPath: details?.resolvedPath,
|
|
2457
|
+
suffixResolution: suffix,
|
|
2458
|
+
fallbackLabel: "image",
|
|
2459
|
+
});
|
|
2416
2460
|
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
2417
2461
|
const header = renderStatusLine(
|
|
2418
2462
|
{ icon: suffix ? "warning" : "success", title: "Read", description: `${displayPath}${correction}` },
|
|
@@ -2421,7 +2465,7 @@ export const readToolRenderer = {
|
|
|
2421
2465
|
const detailLines = contentText ? contentText.split("\n").map(line => uiTheme.fg("toolOutput", line)) : [];
|
|
2422
2466
|
const lines = [...detailLines, ...warningLines];
|
|
2423
2467
|
const outputBlock = new CachedOutputBlock();
|
|
2424
|
-
return {
|
|
2468
|
+
return markFramedBlockComponent({
|
|
2425
2469
|
render: (width: number) =>
|
|
2426
2470
|
outputBlock.render(
|
|
2427
2471
|
{
|
|
@@ -2438,17 +2482,19 @@ export const readToolRenderer = {
|
|
|
2438
2482
|
uiTheme,
|
|
2439
2483
|
),
|
|
2440
2484
|
invalidate: () => outputBlock.invalidate(),
|
|
2441
|
-
};
|
|
2485
|
+
});
|
|
2442
2486
|
}
|
|
2443
2487
|
|
|
2444
2488
|
const suffix = details?.suffixResolution;
|
|
2445
|
-
const plainDisplayPath = suffix ? shortenPath(suffix.to) : filePath;
|
|
2446
2489
|
// resolvedPath is the absolute fs path for fs-backed reads (regular files plus
|
|
2447
2490
|
// local:// / memory:// / skill:// / artifact:// resources). Fall back to a sync
|
|
2448
2491
|
// resolver for fs-backed internal URLs so the title is clickable even before the
|
|
2449
2492
|
// result lands or if the handler didn't populate resolvedPath.
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2493
|
+
const displayPath = formatReadPathLink(rawPath, {
|
|
2494
|
+
resolvedPath: details?.resolvedPath,
|
|
2495
|
+
suffixResolution: suffix,
|
|
2496
|
+
offset: args?.offset,
|
|
2497
|
+
});
|
|
2452
2498
|
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
2453
2499
|
let title = displayPath ? `Read ${displayPath}${correction}` : "Read";
|
|
2454
2500
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
@@ -2463,12 +2509,12 @@ export const readToolRenderer = {
|
|
|
2463
2509
|
const n = details.conflictCount;
|
|
2464
2510
|
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
2465
2511
|
}
|
|
2466
|
-
const rawRequested = args?.raw === true || isRawSelector(parseSel(
|
|
2512
|
+
const rawRequested = args?.raw === true || isRawSelector(parseSel(renderPath.sel));
|
|
2467
2513
|
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
2468
2514
|
let cachedWidth: number | undefined;
|
|
2469
2515
|
let cachedExpanded: boolean | undefined;
|
|
2470
2516
|
let cachedLines: string[] | undefined;
|
|
2471
|
-
return {
|
|
2517
|
+
return markFramedBlockComponent({
|
|
2472
2518
|
render: (width: number) => {
|
|
2473
2519
|
const expanded = options.expanded;
|
|
2474
2520
|
if (cachedLines && cachedWidth === width && cachedExpanded === expanded) return cachedLines;
|
|
@@ -2505,7 +2551,7 @@ export const readToolRenderer = {
|
|
|
2505
2551
|
cachedExpanded = undefined;
|
|
2506
2552
|
cachedLines = undefined;
|
|
2507
2553
|
},
|
|
2508
|
-
};
|
|
2554
|
+
});
|
|
2509
2555
|
},
|
|
2510
2556
|
mergeCallAndResult: true,
|
|
2511
2557
|
};
|
|
@@ -171,6 +171,52 @@ export function formatMoreItems(remaining: number, itemType: string): string {
|
|
|
171
171
|
return `… ${safeRemaining} more ${pluralize(itemType, safeRemaining)}`;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Maximum rows a tool's streaming/pending *call* preview may render before it is
|
|
176
|
+
* capped. This is intentionally conservative: the preview still sits inside a
|
|
177
|
+
* transcript that already consumed some viewport rows, and tool blocks carry
|
|
178
|
+
* extra chrome (status/header/border/"more lines"), so a "reasonable" raw code
|
|
179
|
+
* or command preview like 10-12 lines can still overflow and strand its top
|
|
180
|
+
* while the block is volatile. Keeping the live call window short avoids that
|
|
181
|
+
* across terminals without turning the transcript into an interactive scroller.
|
|
182
|
+
*/
|
|
183
|
+
export const CALL_PREVIEW_MAX_LINES = 6;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cap a pre-rendered pending/call preview to a bounded window. When truncated,
|
|
187
|
+
* show both the head and the live tail so the user can still see what the tool
|
|
188
|
+
* is currently writing while the volatile block stays short enough not to strand
|
|
189
|
+
* its top above the viewport. `Ctrl+O` widens the bounded window, but does not
|
|
190
|
+
* fully uncap live tool previews for the same reason.
|
|
191
|
+
*
|
|
192
|
+
* `prefix` (raw, e.g. a dim tree gutter) is prepended to the summary line so
|
|
193
|
+
* nested previews stay aligned.
|
|
194
|
+
*/
|
|
195
|
+
export function capPreviewLines(
|
|
196
|
+
lines: string[],
|
|
197
|
+
theme: Theme,
|
|
198
|
+
options: { max?: number; expanded?: boolean; prefix?: string } = {},
|
|
199
|
+
): string[] {
|
|
200
|
+
const max = options.max ?? (options.expanded ? PREVIEW_LIMITS.EXPANDED_LINES : CALL_PREVIEW_MAX_LINES);
|
|
201
|
+
if (lines.length <= max) return lines;
|
|
202
|
+
if (max <= 1) {
|
|
203
|
+
const hint = formatExpandHint(theme, options.expanded, true);
|
|
204
|
+
const moreLine = `${formatMoreItems(lines.length, "line")}${hint ? ` ${hint}` : ""}`;
|
|
205
|
+
return [`${options.prefix ?? ""}${theme.fg("dim", moreLine)}`];
|
|
206
|
+
}
|
|
207
|
+
const bodyBudget = max - 1; // reserve one summary row
|
|
208
|
+
const headCount = Math.max(1, Math.ceil(bodyBudget / 2));
|
|
209
|
+
const tailCount = Math.max(1, bodyBudget - headCount);
|
|
210
|
+
const hidden = Math.max(0, lines.length - headCount - tailCount);
|
|
211
|
+
const hint = formatExpandHint(theme, options.expanded, true);
|
|
212
|
+
const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
|
|
213
|
+
return [
|
|
214
|
+
...lines.slice(0, headCount),
|
|
215
|
+
`${options.prefix ?? ""}${theme.fg("dim", moreLine)}`,
|
|
216
|
+
...lines.slice(lines.length - tailCount),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
174
220
|
export function formatMeta(meta: string[], theme: Theme): string {
|
|
175
221
|
return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
|
|
176
222
|
}
|
|
@@ -6,6 +6,7 @@ import * as z from "zod/v4";
|
|
|
6
6
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
7
7
|
import type { Theme } from "../modes/theme/theme";
|
|
8
8
|
import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
|
|
9
|
+
import { resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
9
10
|
import {
|
|
10
11
|
buildDiscoverableToolSearchIndex,
|
|
11
12
|
type DiscoverableTool,
|
|
@@ -198,12 +199,9 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
198
199
|
constructor(private readonly session: ToolSession) {}
|
|
199
200
|
|
|
200
201
|
static createIf(session: ToolSession): SearchToolBm25Tool | null {
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
(toolsDiscoveryMode !== undefined && toolsDiscoveryMode !== "off") ||
|
|
205
|
-
session.settings.get("mcp.discoveryMode") === true;
|
|
206
|
-
if (!active) return null;
|
|
202
|
+
// Direct createTools() calls do not know the final MCP/extension catalog yet, so
|
|
203
|
+
// auto mode is activated later by createAgentSession after the full registry exists.
|
|
204
|
+
if (resolveEffectiveToolDiscoveryMode(session.settings, 0) === "off") return null;
|
|
207
205
|
return supportsToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
|
|
208
206
|
}
|
|
209
207
|
|