@oh-my-pi/pi-coding-agent 15.0.1 → 15.0.2
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 +38 -0
- package/package.json +8 -8
- package/src/commands/commit.ts +10 -0
- package/src/config/model-registry.ts +31 -1
- package/src/config/settings-schema.ts +11 -0
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/modes/acp/acp-agent.ts +248 -50
- package/src/modes/components/status-line/segments.ts +38 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +27 -1
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/hashline.md +22 -26
- package/src/prompts/tools/read.md +55 -37
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +2 -1
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash.ts +39 -15
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/eval.ts +10 -2
- package/src/tools/gh.ts +37 -4
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +26 -0
- package/src/tools/read.ts +32 -4
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +20 -0
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +5 -0
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
|
@@ -1,46 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
Read files, directories, archives, SQLite databases, images, documents, internal resources, and web URLs through a single `path` string.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
|
|
5
|
-
- You
|
|
6
|
-
-
|
|
4
|
+
- One tool for filesystem, archives, SQLite, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), internal URIs, and web URLs (reader-mode by default).
|
|
5
|
+
- You SHOULD parallelize independent reads when exploring related files.
|
|
6
|
+
- You SHOULD reach for `read` — not a browser/puppeteer tool — for fetching web content.
|
|
7
|
+
</instruction>
|
|
7
8
|
|
|
8
9
|
## Parameters
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `mcp://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
10
12
|
|
|
11
13
|
## Selectors
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
16
|
+
|
|
17
|
+
- _(none)_ — parseable code → structural summary (signatures kept, bodies elided); other files → read from the start (up to {{DEFAULT_LIMIT}} lines).
|
|
18
|
+
- `:50` — read from line 50 onward.
|
|
19
|
+
- `:50-200` — lines 50–200 inclusive.
|
|
20
|
+
- `:50+150` — 150 lines starting at line 50.
|
|
21
|
+
- `:20+1` — exactly one line.
|
|
22
|
+
- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).
|
|
23
|
+
- `:raw` — verbatim text; no anchors, no summary, no line prefixes.
|
|
24
|
+
- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.
|
|
25
|
+
- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.
|
|
26
|
+
|
|
27
|
+
# Files
|
|
28
|
+
|
|
29
|
+
- Reading a directory path returns a depth-limited dirent listing.
|
|
30
|
+
{{#if IS_HL_MODE}}
|
|
31
|
+
- Reading a file with an explicit selector returns lines prefixed with `line+hash` anchors: `41th|def alpha():`. The 2-char hash is a content fingerprint that `edit` / `apply_patch` consume — copy it verbatim, NEVER fabricate.
|
|
32
|
+
{{else}}
|
|
33
|
+
{{#if IS_LINE_NUMBER_MODE}}
|
|
34
|
+
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
|
|
35
|
+
{{/if}}
|
|
36
|
+
{{/if}}
|
|
37
|
+
- Parseable code without a selector returns a **structural summary**: declarations kept, large bodies collapsed to `..` (merged brace pair) or `…` (standalone). Summarized output ends with a footer of the form:
|
|
38
|
+
|
|
39
|
+
`[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
- Reading a directory path returns a list of dirents.
|
|
26
|
-
{{#if IS_HL_MODE}}
|
|
27
|
-
- Reading a file with an explicit selector returns lines prefixed with anchors (line+hash): `41th|def alpha():`
|
|
28
|
-
{{else}}
|
|
29
|
-
{{#if IS_LINE_NUMBER_MODE}}
|
|
30
|
-
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`
|
|
31
|
-
{{/if}}
|
|
32
|
-
{{/if}}
|
|
33
|
-
- Reading a parseable code file without a selector returns a structural summary with signatures/declarations kept and large bodies collapsed to `…`. Use `:raw` or an explicit range such as `:1-9999` for verbatim content.
|
|
41
|
+
If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.
|
|
34
42
|
|
|
35
|
-
#
|
|
43
|
+
# Documents & Notebooks
|
|
36
44
|
|
|
37
|
-
Extracts text from PDF, Word, PowerPoint, Excel, RTF,
|
|
45
|
+
Extracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipynb`) are shown as editable `# %% [type] cell:N` text; edits round-trip back to the underlying JSON preserving notebook metadata. Add `:raw` to a notebook to bypass the converter and read the JSON directly.
|
|
38
46
|
|
|
39
|
-
#
|
|
47
|
+
# Images
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
Reading an image path returns metadata (mime, bytes, dimensions, channels, alpha). For actual visual analysis, call `inspect_image` with the path and a question describing what to inspect.
|
|
42
50
|
|
|
43
|
-
#
|
|
51
|
+
# Archives
|
|
52
|
+
|
|
53
|
+
Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read a member, and append a normal selector to the inner path: `archive.zip:dir/file.ts:50-60`.
|
|
54
|
+
|
|
55
|
+
# SQLite
|
|
44
56
|
|
|
45
57
|
For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
46
58
|
- `file.db` — list tables with row counts
|
|
@@ -52,13 +64,19 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
52
64
|
|
|
53
65
|
# URLs
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
- Default reader-mode: HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs → clean text/markdown.
|
|
68
|
+
- `:raw` returns untouched HTML; line selectors (`:50`, `:50-100`, `:50+150`) paginate the cached fetched output.
|
|
69
|
+
- Bare `host:port` URLs collide with the selector grammar — add a trailing slash before the selector: `https://example.com/:80`.
|
|
70
|
+
|
|
71
|
+
# Internal URIs
|
|
72
|
+
|
|
73
|
+
`skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `mcp://<uri>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
57
74
|
|
|
58
75
|
<critical>
|
|
59
|
-
- You MUST use `read` for every file, directory, archive, and URL
|
|
60
|
-
- You MUST prefer `read` over a browser/puppeteer tool for
|
|
61
|
-
- You MUST always include
|
|
62
|
-
- For
|
|
63
|
-
-
|
|
76
|
+
- You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
|
|
77
|
+
- You MUST prefer `read` over a browser/puppeteer tool for URL content; only reach for a browser when `read` cannot deliver reasonable content.
|
|
78
|
+
- You MUST always include `path`. NEVER call `read` with `{}`.
|
|
79
|
+
- For line ranges, append the selector to `path` (`path="src/foo.ts:50-200"`, `path="src/foo.ts:50+150"`). NEVER substitute `sed -n`, `awk NR`, or `head`/`tail` pipelines.
|
|
80
|
+
- Summary footer says `read <path>:raw …`? Re-issue the exact selector it names. NEVER guess what's inside `..` / `…` markers — they carry no content.
|
|
81
|
+
- You MAY combine selectors with URL reads and internal URIs; both paginate the cached resolved output.
|
|
64
82
|
</critical>
|
package/src/task/discovery.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as fs from "node:fs/promises";
|
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { isProviderEnabled } from "../capability";
|
|
18
19
|
import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config";
|
|
19
20
|
import { listClaudePluginRoots } from "../discovery/helpers";
|
|
20
21
|
import { loadBundledAgents, parseAgent } from "./agents";
|
|
@@ -87,8 +88,10 @@ export async function discoverAgents(cwd: string, home: string = os.homedir()):
|
|
|
87
88
|
if (user) orderedDirs.push({ dir: user.path, source: "user" });
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
// Load agents from Claude Code marketplace plugins
|
|
91
|
-
const { roots: pluginRoots } =
|
|
91
|
+
// Load agents from Claude Code marketplace plugins (respects disabledProviders)
|
|
92
|
+
const { roots: pluginRoots } = isProviderEnabled("claude-plugins")
|
|
93
|
+
? await listClaudePluginRoots(home, resolvedCwd)
|
|
94
|
+
: { roots: [] };
|
|
92
95
|
const sortedPluginRoots = [...pluginRoots].sort((a, b) => {
|
|
93
96
|
if (a.scope === b.scope) return 0;
|
|
94
97
|
return a.scope === "project" ? -1 : 1;
|
package/src/task/executor.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Settings } from "../config/settings";
|
|
|
16
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
|
+
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
19
20
|
import type { Skill } from "../extensibility/skills";
|
|
20
21
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
21
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -1119,7 +1120,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1119
1120
|
getAllTools: () => session.getAllToolNames(),
|
|
1120
1121
|
setActiveTools: (toolNames: string[]) =>
|
|
1121
1122
|
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
|
|
1122
|
-
getCommands: () =>
|
|
1123
|
+
getCommands: () => getSessionSlashCommands(session),
|
|
1123
1124
|
setModel: model => runExtensionSetModel(session, model),
|
|
1124
1125
|
getThinkingLevel: () => session.thinkingLevel,
|
|
1125
1126
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative transforms applied to a bash command before execution.
|
|
3
|
+
*
|
|
4
|
+
* Two fixups are applied, each anchored to the end of a top-level segment
|
|
5
|
+
* (segments split on `;`, `&&`, `||`, and background `&`):
|
|
6
|
+
*
|
|
7
|
+
* 1. Trailing `| head [args]` / `| tail [args]` (and the `|&` variant) — these
|
|
8
|
+
* pipes exist purely to limit output length. The harness already truncates
|
|
9
|
+
* bash output and exposes the full result via an artifact, so they only
|
|
10
|
+
* hide content the agent wanted.
|
|
11
|
+
*
|
|
12
|
+
* 2. A redundant trailing `2>&1` left on a segment that has no remaining pipe
|
|
13
|
+
* or other redirect. The harness already merges stderr into stdout, so the
|
|
14
|
+
* duplication is purely cosmetic — and often a leftover after fixup (1)
|
|
15
|
+
* drops a downstream pipe.
|
|
16
|
+
*
|
|
17
|
+
* The heavy lifting (tokenization, quoting, heredoc handling, command
|
|
18
|
+
* substitution, nested compound commands) lives in Rust under
|
|
19
|
+
* `pi_shell::fixup`, driven by the real `brush-parser` AST. This module is a
|
|
20
|
+
* thin sync wrapper plus user-facing notice formatting.
|
|
21
|
+
*/
|
|
22
|
+
import { applyBashFixups as nativeApplyBashFixups } from "@oh-my-pi/pi-natives";
|
|
23
|
+
|
|
24
|
+
export interface BashFixupResult {
|
|
25
|
+
/** Possibly-rewritten command. */
|
|
26
|
+
command: string;
|
|
27
|
+
/** Substrings that were stripped, in the order they were removed. */
|
|
28
|
+
stripped: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply both fixups to a bash command. On any parse failure, multi-line input,
|
|
33
|
+
* or no-op transform, returns the input verbatim with `stripped: []`.
|
|
34
|
+
*/
|
|
35
|
+
export function applyBashFixups(command: string): BashFixupResult {
|
|
36
|
+
return nativeApplyBashFixups(command);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Human-readable notice for the fixups that fired. Mirrors the shape of
|
|
41
|
+
* `formatTimeoutClampNotice` so it can ride alongside the other bash notices.
|
|
42
|
+
*/
|
|
43
|
+
export function formatBashFixupNotice(stripped: readonly string[]): string | undefined {
|
|
44
|
+
if (!stripped.length) return undefined;
|
|
45
|
+
const quoted = stripped.map(s => `\`${s}\``).join(", ");
|
|
46
|
+
return `<system-warning>Stripped redundant ${quoted} — bash output is already truncated and stderr is already merged into stdout. NEVER use these patterns.</system-warning>`;
|
|
47
|
+
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -17,10 +17,11 @@ import { renderStatusLine } from "../tui";
|
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
18
|
import { getSixelLineMask } from "../utils/sixel";
|
|
19
19
|
import type { ToolSession } from ".";
|
|
20
|
+
import { applyBashFixups, formatBashFixupNotice } from "./bash-command-fixup";
|
|
20
21
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
21
22
|
import { checkBashInterception } from "./bash-interceptor";
|
|
22
23
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
23
|
-
import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
24
|
+
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
24
25
|
import { resolveToCwd } from "./path-utils";
|
|
25
26
|
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
26
27
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
@@ -245,6 +246,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
245
246
|
readonly #asyncEnabled: boolean;
|
|
246
247
|
readonly #autoBackgroundEnabled: boolean;
|
|
247
248
|
readonly #autoBackgroundThresholdMs: number;
|
|
249
|
+
#bashFixupNoticeEmitted = false;
|
|
248
250
|
|
|
249
251
|
constructor(private readonly session: ToolSession) {
|
|
250
252
|
this.#asyncEnabled = this.session.settings.get("async.enabled");
|
|
@@ -291,7 +293,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
291
293
|
#buildCompletedResult(
|
|
292
294
|
result: BashResult | BashInteractiveResult,
|
|
293
295
|
timeoutSec: number,
|
|
294
|
-
options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {},
|
|
296
|
+
options: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {},
|
|
295
297
|
): AgentToolResult<BashToolDetails> {
|
|
296
298
|
const outputLines = [this.#formatResultOutput(result)];
|
|
297
299
|
const notices = options.notices?.filter(Boolean) ?? [];
|
|
@@ -314,7 +316,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
314
316
|
label: string,
|
|
315
317
|
previewText: string,
|
|
316
318
|
timeoutSec: number,
|
|
317
|
-
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
319
|
+
options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {},
|
|
318
320
|
): AgentToolResult<BashToolDetails> {
|
|
319
321
|
const details: BashToolDetails = {
|
|
320
322
|
timeoutSeconds: timeoutSec,
|
|
@@ -352,7 +354,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
352
354
|
timeoutMs: number;
|
|
353
355
|
timeoutSec: number;
|
|
354
356
|
requestedTimeoutSec?: number;
|
|
355
|
-
|
|
357
|
+
notices?: readonly string[];
|
|
356
358
|
|
|
357
359
|
resolvedEnv?: Record<string, string>;
|
|
358
360
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
@@ -392,7 +394,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
392
394
|
});
|
|
393
395
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
394
396
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
395
|
-
notices:
|
|
397
|
+
notices: options.notices ?? [],
|
|
396
398
|
});
|
|
397
399
|
const finalText = this.#extractTextResult(finalResult);
|
|
398
400
|
latestText = finalText;
|
|
@@ -483,6 +485,18 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
483
485
|
let command = rawCommand;
|
|
484
486
|
const env = normalizeBashEnv(rawEnv);
|
|
485
487
|
|
|
488
|
+
// Apply conservative bash fixups (strip trailing `| head|tail` and redundant
|
|
489
|
+
// `2>&1`). The helper is single-line only and refuses anything that could
|
|
490
|
+
// change semantics.
|
|
491
|
+
let bashFixups: string[] = [];
|
|
492
|
+
if (this.session.settings.get("bash.stripTrailingHeadTail")) {
|
|
493
|
+
const fixup = applyBashFixups(command);
|
|
494
|
+
if (fixup.stripped.length > 0) {
|
|
495
|
+
command = fixup.command;
|
|
496
|
+
bashFixups = fixup.stripped;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
486
500
|
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
487
501
|
// Constrained to a single line so a `&&` that sits on a later line of a multiline
|
|
488
502
|
// script can't pull the entire script into the "cwd" capture.
|
|
@@ -558,7 +572,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
558
572
|
const requestedTimeoutSec = rawTimeout;
|
|
559
573
|
const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
|
|
560
574
|
const timeoutMs = timeoutSec * 1000;
|
|
575
|
+
const pendingNotices: string[] = [];
|
|
561
576
|
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
577
|
+
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
578
|
+
const bashFixupNotice = this.#bashFixupNoticeEmitted ? undefined : formatBashFixupNotice(bashFixups);
|
|
579
|
+
if (bashFixupNotice) {
|
|
580
|
+
pendingNotices.push(bashFixupNotice);
|
|
581
|
+
this.#bashFixupNoticeEmitted = true;
|
|
582
|
+
}
|
|
562
583
|
|
|
563
584
|
if (asyncRequested) {
|
|
564
585
|
if (!AsyncJobManager.instance()) {
|
|
@@ -570,7 +591,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
570
591
|
timeoutMs,
|
|
571
592
|
timeoutSec,
|
|
572
593
|
requestedTimeoutSec,
|
|
573
|
-
|
|
594
|
+
notices: pendingNotices,
|
|
574
595
|
|
|
575
596
|
resolvedEnv,
|
|
576
597
|
onUpdate,
|
|
@@ -578,7 +599,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
578
599
|
});
|
|
579
600
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
580
601
|
requestedTimeoutSec,
|
|
581
|
-
notices:
|
|
602
|
+
notices: pendingNotices,
|
|
582
603
|
});
|
|
583
604
|
}
|
|
584
605
|
|
|
@@ -592,7 +613,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
592
613
|
timeoutMs,
|
|
593
614
|
timeoutSec,
|
|
594
615
|
requestedTimeoutSec,
|
|
595
|
-
|
|
616
|
+
notices: pendingNotices,
|
|
596
617
|
|
|
597
618
|
resolvedEnv,
|
|
598
619
|
onUpdate,
|
|
@@ -601,7 +622,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
601
622
|
if (startBackgrounded) {
|
|
602
623
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
603
624
|
requestedTimeoutSec,
|
|
604
|
-
notices:
|
|
625
|
+
notices: pendingNotices,
|
|
605
626
|
});
|
|
606
627
|
}
|
|
607
628
|
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
@@ -621,7 +642,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
621
642
|
job.setBackgrounded(true);
|
|
622
643
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
623
644
|
requestedTimeoutSec,
|
|
624
|
-
notices:
|
|
645
|
+
notices: pendingNotices,
|
|
625
646
|
});
|
|
626
647
|
}
|
|
627
648
|
|
|
@@ -722,7 +743,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
722
743
|
};
|
|
723
744
|
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
724
745
|
requestedTimeoutSec,
|
|
725
|
-
notices:
|
|
746
|
+
notices: pendingNotices,
|
|
726
747
|
terminalId: handle.terminalId,
|
|
727
748
|
});
|
|
728
749
|
}
|
|
@@ -778,7 +799,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
778
799
|
|
|
779
800
|
const bridgeNotices: string[] = [];
|
|
780
801
|
if (finalOutput.truncated) bridgeNotices.push("(output truncated)");
|
|
781
|
-
|
|
802
|
+
for (const notice of pendingNotices) bridgeNotices.push(notice);
|
|
782
803
|
|
|
783
804
|
return this.#buildCompletedResult(bridgeResult, timeoutSec, {
|
|
784
805
|
requestedTimeoutSec,
|
|
@@ -833,7 +854,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
833
854
|
}
|
|
834
855
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
835
856
|
requestedTimeoutSec,
|
|
836
|
-
notices:
|
|
857
|
+
notices: pendingNotices,
|
|
837
858
|
});
|
|
838
859
|
}
|
|
839
860
|
}
|
|
@@ -960,8 +981,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
960
981
|
const expanded = renderContext?.expanded ?? options.expanded;
|
|
961
982
|
const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
|
|
962
983
|
|
|
963
|
-
// Get output from context (preferred) or fall back to result content
|
|
964
|
-
|
|
984
|
+
// Get output from context (preferred) or fall back to result content.
|
|
985
|
+
// Strip the LLM-facing notice appended by wrappedExecute so we don't
|
|
986
|
+
// double-print it alongside the styled warning line below.
|
|
987
|
+
const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
988
|
+
const output = stripOutputNotice(rawOutput, details?.meta);
|
|
965
989
|
const displayOutput = output.trimEnd();
|
|
966
990
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
967
991
|
|
|
@@ -11,7 +11,7 @@ import type { RenderResultOptions } from "../../extensibility/custom-tools/types
|
|
|
11
11
|
import type { Theme } from "../../modes/theme/theme";
|
|
12
12
|
import { Hasher, renderCodeCell, renderStatusLine } from "../../tui";
|
|
13
13
|
import type { BrowserToolDetails } from "../browser";
|
|
14
|
-
import { formatStyledTruncationWarning } from "../output-meta";
|
|
14
|
+
import { formatStyledTruncationWarning, stripOutputNotice } from "../output-meta";
|
|
15
15
|
import { replaceTabs, shortenPath } from "../render-utils";
|
|
16
16
|
|
|
17
17
|
const BROWSER_DEFAULT_PREVIEW_LINES = 10;
|
|
@@ -195,7 +195,7 @@ export const browserToolRenderer = {
|
|
|
195
195
|
const details = result.details;
|
|
196
196
|
const action = details?.action ?? argsObj.action;
|
|
197
197
|
const isError = result.isError === true;
|
|
198
|
-
const output = extractTextOutput(result.content);
|
|
198
|
+
const output = stripOutputNotice(extractTextOutput(result.content), details?.meta);
|
|
199
199
|
|
|
200
200
|
if (action === "run") {
|
|
201
201
|
let component = renderRunCell(argsObj, details, options, output, isError, theme);
|
package/src/tools/eval.ts
CHANGED
|
@@ -16,7 +16,12 @@ import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
|
|
16
16
|
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
|
17
17
|
import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
|
|
18
18
|
import { resolveEvalBackends, type ToolSession } from ".";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
formatStyledTruncationWarning,
|
|
21
|
+
resolveOutputMaxColumns,
|
|
22
|
+
resolveOutputSinkHeadBytes,
|
|
23
|
+
stripOutputNotice,
|
|
24
|
+
} from "./output-meta";
|
|
20
25
|
import { formatTitle, replaceTabs, shortenPath, truncateToWidth, wrapBrackets } from "./render-utils";
|
|
21
26
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
22
27
|
import { toolResult } from "./tool-result";
|
|
@@ -922,8 +927,11 @@ export const evalToolRenderer = {
|
|
|
922
927
|
): Component {
|
|
923
928
|
const details = result.details;
|
|
924
929
|
|
|
925
|
-
const
|
|
930
|
+
const rawOutput =
|
|
926
931
|
options.renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
|
|
932
|
+
// Strip the LLM-facing notice (appended by wrappedExecute) before display;
|
|
933
|
+
// the styled `warningLine` below carries the same text in ⟨…⟩ form.
|
|
934
|
+
const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
|
|
927
935
|
|
|
928
936
|
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
929
937
|
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
package/src/tools/gh.ts
CHANGED
|
@@ -1774,6 +1774,39 @@ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSign
|
|
|
1774
1774
|
return untilAborted(signal, pending);
|
|
1775
1775
|
}
|
|
1776
1776
|
|
|
1777
|
+
/**
|
|
1778
|
+
* Matches search-query qualifiers that already scope to a repository, org, or
|
|
1779
|
+
* user. When present, callers should avoid layering a default `repo:<current>`
|
|
1780
|
+
* on top — the user has already expressed an explicit scope.
|
|
1781
|
+
*
|
|
1782
|
+
* Only the leading `repo:`/`org:`/`user:`/`owner:` token is treated as a
|
|
1783
|
+
* scope marker; arbitrary substrings (e.g. inside quoted text) are ignored.
|
|
1784
|
+
*/
|
|
1785
|
+
const REPO_SCOPE_QUALIFIER_PATTERN = /(?:^|\s)-?(?:repo|org|user|owner):\S/i;
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Resolve the effective `repo:` scope for a search op. Returns the explicit
|
|
1789
|
+
* `repo` when set, `undefined` when the query already carries a scoping
|
|
1790
|
+
* qualifier, and otherwise the current checkout's `owner/repo` via
|
|
1791
|
+
* `resolveDefaultRepoMemoized`. Resolution failures (no git/gh context, no
|
|
1792
|
+
* configured remote) silently fall back to `undefined` so the search proceeds
|
|
1793
|
+
* across all of GitHub instead of throwing.
|
|
1794
|
+
*/
|
|
1795
|
+
async function resolveSearchRepoScope(
|
|
1796
|
+
cwd: string,
|
|
1797
|
+
repo: string | undefined,
|
|
1798
|
+
query: string | undefined,
|
|
1799
|
+
signal: AbortSignal | undefined,
|
|
1800
|
+
): Promise<string | undefined> {
|
|
1801
|
+
if (repo) return repo;
|
|
1802
|
+
if (query && REPO_SCOPE_QUALIFIER_PATTERN.test(query)) return undefined;
|
|
1803
|
+
try {
|
|
1804
|
+
return await resolveDefaultRepoMemoized(cwd, signal);
|
|
1805
|
+
} catch {
|
|
1806
|
+
return undefined;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1777
1810
|
async function resolveGitHubBranchHead(
|
|
1778
1811
|
cwd: string,
|
|
1779
1812
|
repo: string,
|
|
@@ -3267,11 +3300,11 @@ async function executeSearchIssues(
|
|
|
3267
3300
|
params: GithubInput,
|
|
3268
3301
|
signal: AbortSignal | undefined,
|
|
3269
3302
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3270
|
-
const repo = normalizeOptionalString(params.repo);
|
|
3271
3303
|
const limit = resolveSearchLimit(params.limit);
|
|
3272
3304
|
const dateField = resolveSearchDateField("issues", params.dateField);
|
|
3273
3305
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
3274
3306
|
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3307
|
+
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
|
3275
3308
|
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:issue"]);
|
|
3276
3309
|
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
3277
3310
|
|
|
@@ -3285,11 +3318,11 @@ async function executeSearchPrs(
|
|
|
3285
3318
|
params: GithubInput,
|
|
3286
3319
|
signal: AbortSignal | undefined,
|
|
3287
3320
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3288
|
-
const repo = normalizeOptionalString(params.repo);
|
|
3289
3321
|
const limit = resolveSearchLimit(params.limit);
|
|
3290
3322
|
const dateField = resolveSearchDateField("prs", params.dateField);
|
|
3291
3323
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
3292
3324
|
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3325
|
+
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
|
3293
3326
|
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:pr"]);
|
|
3294
3327
|
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
3295
3328
|
|
|
@@ -3307,8 +3340,8 @@ async function executeSearchCode(
|
|
|
3307
3340
|
if (params.since !== undefined || params.until !== undefined) {
|
|
3308
3341
|
throw new ToolError("search_code does not support since/until; GitHub code search has no date qualifier.");
|
|
3309
3342
|
}
|
|
3310
|
-
const repo = normalizeOptionalString(params.repo);
|
|
3311
3343
|
const limit = resolveSearchLimit(params.limit);
|
|
3344
|
+
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), query, signal);
|
|
3312
3345
|
const apiQuery = composeSearchQuery([query, repo ? `repo:${repo}` : undefined]);
|
|
3313
3346
|
const args = buildGhApiSearchArgs("code", apiQuery, limit, ["Accept: application/vnd.github.text-match+json"]);
|
|
3314
3347
|
|
|
@@ -3322,11 +3355,11 @@ async function executeSearchCommits(
|
|
|
3322
3355
|
params: GithubInput,
|
|
3323
3356
|
signal: AbortSignal | undefined,
|
|
3324
3357
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3325
|
-
const repo = normalizeOptionalString(params.repo);
|
|
3326
3358
|
const limit = resolveSearchLimit(params.limit);
|
|
3327
3359
|
const dateField = resolveSearchDateField("commits", params.dateField);
|
|
3328
3360
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
3329
3361
|
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3362
|
+
const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
|
|
3330
3363
|
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined]);
|
|
3331
3364
|
const args = buildGhApiSearchArgs("commits", apiQuery, limit);
|
|
3332
3365
|
|
package/src/tools/job.ts
CHANGED
|
@@ -362,6 +362,8 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
|
362
362
|
const LABEL_MAX_WIDTH = 60;
|
|
363
363
|
const PREVIEW_LINES_COLLAPSED = 1;
|
|
364
364
|
const PREVIEW_LINES_EXPANDED = 4;
|
|
365
|
+
const LABEL_LINES_COLLAPSED = 1;
|
|
366
|
+
const LABEL_LINES_EXPANDED = 3;
|
|
365
367
|
const PREVIEW_LINE_WIDTH = 80;
|
|
366
368
|
|
|
367
369
|
function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
|
|
@@ -488,14 +490,21 @@ export const jobToolRenderer = {
|
|
|
488
490
|
);
|
|
489
491
|
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
490
492
|
const idText = uiTheme.fg("muted", job.id);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
|
|
494
|
+
const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
|
|
495
|
+
const visibleLabelLines = rawLabelLines
|
|
496
|
+
.slice(0, maxLabelLines)
|
|
497
|
+
.map(l => truncateToWidth(replaceTabs(l), LABEL_MAX_WIDTH, Ellipsis.Unicode));
|
|
498
|
+
if (rawLabelLines.length > maxLabelLines && visibleLabelLines.length > 0) {
|
|
499
|
+
const last = visibleLabelLines[visibleLabelLines.length - 1]!;
|
|
500
|
+
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
501
|
+
}
|
|
497
502
|
const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
|
|
498
|
-
|
|
503
|
+
const headLabel = uiTheme.fg("toolOutput", visibleLabelLines[0] ?? "");
|
|
504
|
+
lines.push(`${icon} ${idText} ${typeBadge} ${headLabel} ${durationText}`);
|
|
505
|
+
for (let i = 1; i < visibleLabelLines.length; i++) {
|
|
506
|
+
lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
|
|
507
|
+
}
|
|
499
508
|
|
|
500
509
|
const preview = job.errorText?.trim() || job.resultText?.trim();
|
|
501
510
|
if (preview) {
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -489,6 +489,32 @@ export function formatStyledTruncationWarning(meta: OutputMeta | undefined, them
|
|
|
489
489
|
return theme.fg("warning", wrapBrackets(message, theme));
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Strip the trailing notice that {@link appendOutputNotice} bakes into the
|
|
494
|
+
* LLM-facing content body. Renderers should call this before printing
|
|
495
|
+
* `result.content` text in the TUI, because they emit a styled warning line of
|
|
496
|
+
* their own; without this, users see the same `[Showing lines …]` string twice
|
|
497
|
+
* (once verbatim from the body, once as the styled `⟨…⟩` warning).
|
|
498
|
+
*
|
|
499
|
+
* Safe to call eagerly: returns the input unchanged when no notice is present
|
|
500
|
+
* (e.g. during streaming, before {@link wrappedExecute} runs).
|
|
501
|
+
*/
|
|
502
|
+
export function stripOutputNotice(text: string, meta: OutputMeta | undefined): string {
|
|
503
|
+
const notice = formatOutputNotice(meta);
|
|
504
|
+
if (!notice) return text;
|
|
505
|
+
// Trim trailing whitespace from `text` and from the notice itself so we
|
|
506
|
+
// match regardless of whether: (a) the caller already trimEnd()'d, (b)
|
|
507
|
+
// extra blank lines slipped in after the notice (diagnostics blocks add
|
|
508
|
+
// `\n\n` between sections, OutputSink may pad), or (c) neither. Returns
|
|
509
|
+
// the prefix before the notice so the caller can re-trim as needed.
|
|
510
|
+
const trimmedText = text.trimEnd();
|
|
511
|
+
const trimmedNotice = notice.trimEnd();
|
|
512
|
+
if (trimmedText.endsWith(trimmedNotice)) {
|
|
513
|
+
return trimmedText.slice(0, -trimmedNotice.length);
|
|
514
|
+
}
|
|
515
|
+
return text;
|
|
516
|
+
}
|
|
517
|
+
|
|
492
518
|
// =============================================================================
|
|
493
519
|
// Tool wrapper
|
|
494
520
|
// =============================================================================
|