@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
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 +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/tools/ast-grep.ts
CHANGED
|
@@ -6,34 +6,28 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
10
9
|
import type { Theme } from "../modes/theme/theme";
|
|
11
10
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
12
|
-
import { Ellipsis,
|
|
11
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
13
12
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
14
13
|
import type { ToolSession } from ".";
|
|
15
14
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
16
15
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
17
16
|
import { formatMatchLine } from "./match-line-format";
|
|
18
17
|
import type { OutputMeta } from "./output-meta";
|
|
18
|
+
import { resolveToolSearchScope } from "./path-utils";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
normalizePathLikeInput,
|
|
23
|
-
parseSearchPath,
|
|
24
|
-
partitionExistingPaths,
|
|
25
|
-
resolveExplicitSearchPaths,
|
|
26
|
-
resolveToCwd,
|
|
27
|
-
} from "./path-utils";
|
|
28
|
-
import {
|
|
20
|
+
appendParseErrorsBulletList,
|
|
21
|
+
createCachedComponent,
|
|
29
22
|
dedupeParseErrors,
|
|
30
23
|
formatCodeFrameLine,
|
|
31
24
|
formatCount,
|
|
32
25
|
formatEmptyMessage,
|
|
33
26
|
formatErrorMessage,
|
|
34
27
|
formatParseErrors,
|
|
35
|
-
|
|
28
|
+
formatParseErrorsCountLabel,
|
|
36
29
|
PREVIEW_LIMITS,
|
|
30
|
+
splitGroupsByBlankLine,
|
|
37
31
|
} from "./render-utils";
|
|
38
32
|
import { ToolError } from "./tool-errors";
|
|
39
33
|
import { toolResult } from "./tool-result";
|
|
@@ -150,64 +144,12 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
150
144
|
if (!Number.isFinite(skip) || skip < 0) {
|
|
151
145
|
throw new ToolError("skip must be a non-negative number");
|
|
152
146
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
160
|
-
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
161
|
-
}
|
|
162
|
-
const internalRouter = InternalUrlRouter.instance();
|
|
163
|
-
const resolvedPathInputs: string[] = [];
|
|
164
|
-
for (const rawPath of rawPaths) {
|
|
165
|
-
if (!internalRouter.canHandle(rawPath)) {
|
|
166
|
-
resolvedPathInputs.push(rawPath);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (hasGlobPathChars(rawPath)) {
|
|
170
|
-
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
171
|
-
}
|
|
172
|
-
const resource = await internalRouter.resolve(rawPath);
|
|
173
|
-
if (!resource.sourcePath) {
|
|
174
|
-
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
|
175
|
-
}
|
|
176
|
-
resolvedPathInputs.push(resource.sourcePath);
|
|
177
|
-
}
|
|
178
|
-
let effectivePathInputs = resolvedPathInputs;
|
|
179
|
-
if (resolvedPathInputs.length > 1) {
|
|
180
|
-
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
181
|
-
if (partition.valid.length === 0) {
|
|
182
|
-
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
183
|
-
}
|
|
184
|
-
effectivePathInputs = partition.valid;
|
|
185
|
-
}
|
|
186
|
-
if (effectivePathInputs.length === 1) {
|
|
187
|
-
const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
|
|
188
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
189
|
-
globFilter = parsedPath.glob;
|
|
190
|
-
scopePath = formatScopePath(searchPath);
|
|
191
|
-
} else {
|
|
192
|
-
const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
|
|
193
|
-
if (!multiSearchPath) {
|
|
194
|
-
throw new ToolError("`paths` must contain at least one path or glob");
|
|
195
|
-
}
|
|
196
|
-
searchPath = multiSearchPath.basePath;
|
|
197
|
-
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
198
|
-
multiTargets = multiSearchPath.targets;
|
|
199
|
-
scopePath = multiSearchPath.scopePath;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const resolvedSearchPath = searchPath;
|
|
203
|
-
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
204
|
-
let isDirectory: boolean;
|
|
205
|
-
try {
|
|
206
|
-
const stat = await Bun.file(resolvedSearchPath).stat();
|
|
207
|
-
isDirectory = stat.isDirectory();
|
|
208
|
-
} catch {
|
|
209
|
-
throw new ToolError(`Path not found: ${scopePath}`);
|
|
210
|
-
}
|
|
147
|
+
const scope = await resolveToolSearchScope({
|
|
148
|
+
rawPaths: params.paths,
|
|
149
|
+
cwd: this.session.cwd,
|
|
150
|
+
internalUrlAction: "search",
|
|
151
|
+
});
|
|
152
|
+
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
211
153
|
|
|
212
154
|
const DEFAULT_AST_LIMIT = 50;
|
|
213
155
|
const result = multiTargets
|
|
@@ -388,13 +330,7 @@ export const astGrepToolRenderer = {
|
|
|
388
330
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
389
331
|
if (details?.parseErrors?.length) {
|
|
390
332
|
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `paths` before concluding absence"));
|
|
391
|
-
|
|
392
|
-
for (const err of capped) {
|
|
393
|
-
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
|
394
|
-
}
|
|
395
|
-
if (details.parseErrors.length > PARSE_ERRORS_LIMIT) {
|
|
396
|
-
lines.push(uiTheme.fg("dim", ` … ${details.parseErrors.length - PARSE_ERRORS_LIMIT} more`));
|
|
397
|
-
}
|
|
333
|
+
appendParseErrorsBulletList(lines, details.parseErrors, uiTheme);
|
|
398
334
|
}
|
|
399
335
|
return new Text(lines.join("\n"), 0, 0);
|
|
400
336
|
}
|
|
@@ -411,28 +347,7 @@ export const astGrepToolRenderer = {
|
|
|
411
347
|
);
|
|
412
348
|
|
|
413
349
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
414
|
-
const
|
|
415
|
-
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
416
|
-
const allGroups: string[][] = [];
|
|
417
|
-
if (hasSeparators) {
|
|
418
|
-
let current: string[] = [];
|
|
419
|
-
for (const line of rawLines) {
|
|
420
|
-
if (line.trim().length === 0) {
|
|
421
|
-
if (current.length > 0) {
|
|
422
|
-
allGroups.push(current);
|
|
423
|
-
current = [];
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
current.push(line);
|
|
428
|
-
}
|
|
429
|
-
if (current.length > 0) allGroups.push(current);
|
|
430
|
-
} else {
|
|
431
|
-
const nonEmpty = rawLines.filter(line => line.trim().length > 0);
|
|
432
|
-
if (nonEmpty.length > 0) {
|
|
433
|
-
allGroups.push(nonEmpty);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
350
|
+
const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
|
|
436
351
|
const matchGroups = allGroups.filter(
|
|
437
352
|
group => !group[0]?.startsWith("Result limit reached") && !group[0]?.startsWith("Parse issues:"),
|
|
438
353
|
);
|
|
@@ -442,24 +357,16 @@ export const astGrepToolRenderer = {
|
|
|
442
357
|
extraLines.push(uiTheme.fg("warning", "limit reached; narrow paths or increase limit"));
|
|
443
358
|
}
|
|
444
359
|
if (details?.parseErrors?.length) {
|
|
445
|
-
|
|
446
|
-
const label =
|
|
447
|
-
total > PARSE_ERRORS_LIMIT
|
|
448
|
-
? `${PARSE_ERRORS_LIMIT} / ${total} parse issues`
|
|
449
|
-
: `${total} parse issue${total !== 1 ? "s" : ""}`;
|
|
450
|
-
extraLines.push(uiTheme.fg("warning", label));
|
|
360
|
+
extraLines.push(uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors)));
|
|
451
361
|
}
|
|
452
362
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const { expanded } = options;
|
|
457
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
458
|
-
if (cached?.key === key) return cached.lines;
|
|
363
|
+
return createCachedComponent(
|
|
364
|
+
() => options.expanded,
|
|
365
|
+
width => {
|
|
459
366
|
const matchLines = renderTreeList(
|
|
460
367
|
{
|
|
461
368
|
items: matchGroups,
|
|
462
|
-
expanded,
|
|
369
|
+
expanded: options.expanded,
|
|
463
370
|
maxCollapsed: matchGroups.length,
|
|
464
371
|
maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
|
|
465
372
|
itemType: "match",
|
|
@@ -473,14 +380,9 @@ export const astGrepToolRenderer = {
|
|
|
473
380
|
},
|
|
474
381
|
uiTheme,
|
|
475
382
|
);
|
|
476
|
-
|
|
477
|
-
cached = { key, lines: rendered };
|
|
478
|
-
return rendered;
|
|
383
|
+
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
479
384
|
},
|
|
480
|
-
|
|
481
|
-
cached = undefined;
|
|
482
|
-
},
|
|
483
|
-
};
|
|
385
|
+
);
|
|
484
386
|
},
|
|
485
387
|
mergeCallAndResult: true,
|
|
486
388
|
};
|
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
|
14
14
|
import xterm from "@xterm/headless";
|
|
15
|
+
import { Settings } from "../config/settings";
|
|
15
16
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
16
17
|
import type { Theme } from "../modes/theme/theme";
|
|
17
18
|
import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
|
18
19
|
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
20
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
|
|
19
21
|
import { formatStatusIcon, replaceTabs } from "./render-utils";
|
|
20
22
|
|
|
21
23
|
export interface BashInteractiveResult extends OutputSummary {
|
|
@@ -294,7 +296,13 @@ export async function runInteractiveBashPty(
|
|
|
294
296
|
artifactId?: string;
|
|
295
297
|
},
|
|
296
298
|
): Promise<BashInteractiveResult> {
|
|
297
|
-
const
|
|
299
|
+
const settings = await Settings.init();
|
|
300
|
+
const sink = new OutputSink({
|
|
301
|
+
artifactPath: options.artifactPath,
|
|
302
|
+
artifactId: options.artifactId,
|
|
303
|
+
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
304
|
+
maxColumns: resolveOutputMaxColumns(settings),
|
|
305
|
+
});
|
|
298
306
|
const result = await ui.custom<BashInteractiveResult>(
|
|
299
307
|
(tui, uiTheme, _keybindings, done) => {
|
|
300
308
|
const session = new PtySession();
|
package/src/tools/bash.ts
CHANGED
|
@@ -2,15 +2,16 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { $env, getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import { AsyncJobManager } from "../async";
|
|
8
8
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import { InternalUrlRouter } from "../internal-urls";
|
|
11
11
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
12
|
-
import type
|
|
12
|
+
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
13
13
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
14
|
+
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
14
15
|
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
15
16
|
import { renderStatusLine } from "../tui";
|
|
16
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
@@ -84,6 +85,7 @@ export interface BashToolDetails {
|
|
|
84
85
|
meta?: OutputMeta;
|
|
85
86
|
timeoutSeconds?: number;
|
|
86
87
|
requestedTimeoutSeconds?: number;
|
|
88
|
+
terminalId?: string;
|
|
87
89
|
async?: {
|
|
88
90
|
state: "running" | "completed" | "failed";
|
|
89
91
|
jobId: string;
|
|
@@ -289,7 +291,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
289
291
|
#buildCompletedResult(
|
|
290
292
|
result: BashResult | BashInteractiveResult,
|
|
291
293
|
timeoutSec: number,
|
|
292
|
-
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
294
|
+
options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {},
|
|
293
295
|
): AgentToolResult<BashToolDetails> {
|
|
294
296
|
const outputLines = [this.#formatResultOutput(result)];
|
|
295
297
|
const notices = options.notices?.filter(Boolean) ?? [];
|
|
@@ -299,6 +301,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
299
301
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
300
302
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
301
303
|
}
|
|
304
|
+
if (options.terminalId !== undefined) {
|
|
305
|
+
details.terminalId = options.terminalId;
|
|
306
|
+
}
|
|
302
307
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
303
308
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
304
309
|
return resultBuilder.done();
|
|
@@ -479,8 +484,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
479
484
|
const env = normalizeBashEnv(rawEnv);
|
|
480
485
|
|
|
481
486
|
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
487
|
+
// Constrained to a single line so a `&&` that sits on a later line of a multiline
|
|
488
|
+
// script can't pull the entire script into the "cwd" capture.
|
|
482
489
|
if (!cwd) {
|
|
483
|
-
const cdMatch = command.match(/^cd\
|
|
490
|
+
const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
|
|
484
491
|
if (cdMatch) {
|
|
485
492
|
cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
486
493
|
command = command.slice(cdMatch[0].length);
|
|
@@ -618,6 +625,175 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
618
625
|
});
|
|
619
626
|
}
|
|
620
627
|
|
|
628
|
+
// Route through the client terminal when the client advertises the terminal capability.
|
|
629
|
+
// Skip when pty=true (PTY needs the local terminal UI).
|
|
630
|
+
const clientBridge = this.session.getClientBridge?.();
|
|
631
|
+
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
632
|
+
const handle = await clientBridge.createTerminal({
|
|
633
|
+
command,
|
|
634
|
+
cwd: commandCwd,
|
|
635
|
+
env: resolvedEnv
|
|
636
|
+
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
637
|
+
: undefined,
|
|
638
|
+
outputByteLimit: DEFAULT_MAX_BYTES,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Emit partial update so the editor can embed the live terminal card.
|
|
642
|
+
onUpdate?.({ content: [], details: { terminalId: handle.terminalId } });
|
|
643
|
+
|
|
644
|
+
const exitPromise = handle.waitForExit();
|
|
645
|
+
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
646
|
+
|
|
647
|
+
type BridgeRaceResult =
|
|
648
|
+
| { kind: "exit"; status: ClientBridgeTerminalExitStatus }
|
|
649
|
+
| { kind: "poll" }
|
|
650
|
+
| { kind: "timeout" }
|
|
651
|
+
| { kind: "aborted" };
|
|
652
|
+
|
|
653
|
+
// Set up abort listener before entering the poll loop. The listener
|
|
654
|
+
// kicks off `handle.kill()` synchronously so a `session/cancel`
|
|
655
|
+
// arriving mid-poll terminates the remote command immediately,
|
|
656
|
+
// instead of waiting for the next `currentOutput()` to return.
|
|
657
|
+
const { promise: abortedP, resolve: resolveAborted } = Promise.withResolvers<void>();
|
|
658
|
+
let killStarted = false;
|
|
659
|
+
const fireKill = (): Promise<void> => {
|
|
660
|
+
if (killStarted) return Promise.resolve();
|
|
661
|
+
killStarted = true;
|
|
662
|
+
return handle.kill().catch((error: unknown) => {
|
|
663
|
+
logger.warn("ACP terminal kill failed", { terminalId: handle.terminalId, error });
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
const onAbortSignal = () => {
|
|
667
|
+
resolveAborted();
|
|
668
|
+
void fireKill();
|
|
669
|
+
};
|
|
670
|
+
signal?.addEventListener("abort", onAbortSignal, { once: true });
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
try {
|
|
674
|
+
if (signal?.aborted) {
|
|
675
|
+
await fireKill();
|
|
676
|
+
throw new ToolAbortError("Command aborted");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const timeoutPromise = Bun.sleep(timeoutMs).then(() => ({ kind: "timeout" as const }));
|
|
680
|
+
// Poll until the process exits, times out, or the caller aborts.
|
|
681
|
+
for (;;) {
|
|
682
|
+
const racers: Array<Promise<BridgeRaceResult>> = [
|
|
683
|
+
exitPromise.then(s => ({ kind: "exit" as const, status: s })),
|
|
684
|
+
timeoutPromise,
|
|
685
|
+
Bun.sleep(250).then(() => ({ kind: "poll" as const })),
|
|
686
|
+
];
|
|
687
|
+
if (signal) {
|
|
688
|
+
racers.push(abortedP.then(() => ({ kind: "aborted" as const })));
|
|
689
|
+
}
|
|
690
|
+
const raced = await Promise.race(racers);
|
|
691
|
+
|
|
692
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
693
|
+
await fireKill();
|
|
694
|
+
throw new ToolAbortError("Command aborted");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (raced.kind === "timeout") {
|
|
698
|
+
// Kill before reading final output so a slow `terminal/output`
|
|
699
|
+
// RPC cannot let a timed-out command keep running past the
|
|
700
|
+
// enforced timeout. The handle stays valid post-kill so the
|
|
701
|
+
// buffered output is still readable.
|
|
702
|
+
await fireKill();
|
|
703
|
+
let current = { output: "", truncated: false };
|
|
704
|
+
try {
|
|
705
|
+
current = await handle.currentOutput();
|
|
706
|
+
} catch (error) {
|
|
707
|
+
logger.warn("ACP terminal final output read failed", {
|
|
708
|
+
terminalId: handle.terminalId,
|
|
709
|
+
error,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
const timedOutResult: BashInteractiveResult = {
|
|
713
|
+
output: current.output,
|
|
714
|
+
exitCode: undefined,
|
|
715
|
+
cancelled: false,
|
|
716
|
+
timedOut: true,
|
|
717
|
+
truncated: current.truncated,
|
|
718
|
+
totalLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
719
|
+
totalBytes: current.output.length,
|
|
720
|
+
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
721
|
+
outputBytes: current.output.length,
|
|
722
|
+
};
|
|
723
|
+
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
724
|
+
requestedTimeoutSec,
|
|
725
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
726
|
+
terminalId: handle.terminalId,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (raced.kind === "exit") {
|
|
731
|
+
exitStatus = raced.status;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Poll tick: push current output so agent-loop transcript stays consistent.
|
|
736
|
+
// Race the read against abort so a stuck `terminal/output` RPC does not
|
|
737
|
+
// delay cancellation.
|
|
738
|
+
const pollOutput = await Promise.race([
|
|
739
|
+
handle.currentOutput(),
|
|
740
|
+
abortedP.then(() => undefined as ClientBridgeTerminalOutput | undefined),
|
|
741
|
+
]);
|
|
742
|
+
if (pollOutput === undefined) {
|
|
743
|
+
// Abort fired during the poll-tick read; let the next loop iteration
|
|
744
|
+
// observe `signal?.aborted` and exit via the abort branch.
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
onUpdate?.({
|
|
748
|
+
content: [{ type: "text", text: pollOutput.output }],
|
|
749
|
+
details: { terminalId: handle.terminalId },
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
} finally {
|
|
753
|
+
signal?.removeEventListener("abort", onAbortSignal);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Fetch final output; the terminal is released in the outer finally.
|
|
757
|
+
const finalOutput = await handle.currentOutput();
|
|
758
|
+
|
|
759
|
+
// Map exit status: null exitCode with a signal → treat as signal kill (137).
|
|
760
|
+
const rawExitCode = exitStatus.exitCode;
|
|
761
|
+
const exitCode: number | undefined =
|
|
762
|
+
rawExitCode != null ? rawExitCode : exitStatus.signal ? 137 : undefined;
|
|
763
|
+
|
|
764
|
+
const outputText = finalOutput.output;
|
|
765
|
+
const outputByteLen = outputText.length;
|
|
766
|
+
const outputLineCount = outputText.length > 0 ? outputText.split("\n").length : 0;
|
|
767
|
+
|
|
768
|
+
const bridgeResult: BashResult = {
|
|
769
|
+
output: outputText,
|
|
770
|
+
exitCode,
|
|
771
|
+
cancelled: false,
|
|
772
|
+
truncated: finalOutput.truncated,
|
|
773
|
+
totalLines: outputLineCount,
|
|
774
|
+
totalBytes: outputByteLen,
|
|
775
|
+
outputLines: outputLineCount,
|
|
776
|
+
outputBytes: outputByteLen,
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const bridgeNotices: string[] = [];
|
|
780
|
+
if (finalOutput.truncated) bridgeNotices.push("(output truncated)");
|
|
781
|
+
if (timeoutClampNotice) bridgeNotices.push(timeoutClampNotice);
|
|
782
|
+
|
|
783
|
+
return this.#buildCompletedResult(bridgeResult, timeoutSec, {
|
|
784
|
+
requestedTimeoutSec,
|
|
785
|
+
notices: bridgeNotices,
|
|
786
|
+
terminalId: handle.terminalId,
|
|
787
|
+
});
|
|
788
|
+
} finally {
|
|
789
|
+
try {
|
|
790
|
+
await handle.release();
|
|
791
|
+
} catch (error) {
|
|
792
|
+
logger.warn("ACP terminal release failed", { terminalId: handle.terminalId, error });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
621
797
|
// Track output for streaming updates (tail only)
|
|
622
798
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
623
799
|
|
|
@@ -718,6 +894,27 @@ export function formatBashCommand(args: BashRenderArgs): string {
|
|
|
718
894
|
return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
|
|
719
895
|
}
|
|
720
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Returns the bash command formatted for the result body: the dim `$ cd … &&`
|
|
899
|
+
* prefix joined with syntax-highlighted command lines. The prefix is applied
|
|
900
|
+
* only to the first line so multi-line commands display cleanly — terminals
|
|
901
|
+
* reset SGR state at line boundaries, which made the previous single-string
|
|
902
|
+
* `theme.fg("dim", ...)` form render only the first line as dim.
|
|
903
|
+
*/
|
|
904
|
+
export function formatBashCommandLines(args: BashRenderArgs, uiTheme: Theme): string[] {
|
|
905
|
+
const command = replaceTabs(args.command || "…");
|
|
906
|
+
const cwd = getProjectDir();
|
|
907
|
+
const displayWorkdir = formatToolWorkingDirectory(args.cwd, cwd);
|
|
908
|
+
const envAssignments = formatBashEnvAssignments(getBashEnvForDisplay(args));
|
|
909
|
+
const prefixParts = ["$"];
|
|
910
|
+
if (displayWorkdir) prefixParts.push(`cd ${displayWorkdir} &&`);
|
|
911
|
+
if (envAssignments) prefixParts.push(envAssignments);
|
|
912
|
+
const prefix = uiTheme.fg("dim", `${prefixParts.join(" ")} `);
|
|
913
|
+
const highlightedLines = highlightCode(command, "bash");
|
|
914
|
+
if (highlightedLines.length === 0) return [prefix.trimEnd()];
|
|
915
|
+
return highlightedLines.map((line, i) => (i === 0 ? `${prefix}${line}` : line));
|
|
916
|
+
}
|
|
917
|
+
|
|
721
918
|
function toBashRenderArgs<TArgs>(args: TArgs | undefined, config: ShellRendererConfig<TArgs>): BashRenderArgs {
|
|
722
919
|
return {
|
|
723
920
|
command: config.resolveCommand?.(args),
|
|
@@ -748,7 +945,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
748
945
|
args?: TArgs,
|
|
749
946
|
): Component {
|
|
750
947
|
const renderArgs = toBashRenderArgs(args, config);
|
|
751
|
-
const
|
|
948
|
+
const cmdLines = args ? formatBashCommandLines(renderArgs, uiTheme) : undefined;
|
|
752
949
|
const isError = result.isError === true;
|
|
753
950
|
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
754
951
|
const title = config.resolveTitle(args, options);
|
|
@@ -826,7 +1023,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
826
1023
|
header,
|
|
827
1024
|
state: options.isPartial ? "pending" : isError ? "error" : "success",
|
|
828
1025
|
sections: [
|
|
829
|
-
{ lines:
|
|
1026
|
+
{ lines: cmdLines ?? [] },
|
|
830
1027
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
831
1028
|
],
|
|
832
1029
|
width,
|
|
@@ -3,7 +3,7 @@ import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
|
3
3
|
import type { Browser, Page } from "puppeteer-core";
|
|
4
4
|
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const ATTACH_TARGET_SKIP_PATTERN =
|
|
7
7
|
/request[\s_-]?handler|devtools|background[\s_-]?(?:page|host)|service[\s_-]?worker/i;
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -62,7 +62,7 @@ export async function waitForCdp(cdpUrl: string, timeoutMs: number, signal?: Abo
|
|
|
62
62
|
* accepts both `--flag=value` and `--flag value`). Returns null if absent or
|
|
63
63
|
* malformed.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
function findCdpPortInArgs(args: string[]): number | null {
|
|
66
66
|
for (const arg of args) {
|
|
67
67
|
const m = /^--remote-debugging-port=(\d+)$/.exec(arg);
|
|
68
68
|
if (m) {
|
|
@@ -80,7 +80,7 @@ export function findCdpPortInArgs(args: string[]): number | null {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** One-shot probe: returns true when `/json/version` answers 200 within the timeout. */
|
|
83
|
-
|
|
83
|
+
async function probeCdpAt(port: number, signal?: AbortSignal): Promise<boolean> {
|
|
84
84
|
const probeTimeout = AbortSignal.timeout(1500);
|
|
85
85
|
const probeSignal = signal ? AbortSignal.any([signal, probeTimeout]) : probeTimeout;
|
|
86
86
|
try {
|