@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 +75 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +15 -5
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +18 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +4 -0
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/dry-balance-cli.ts +52 -17
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +33 -9
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-registry.ts +25 -2
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +20 -2
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +40 -54
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +90 -31
- package/src/eval/llm-bridge.ts +8 -3
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +9 -7
- package/src/memories/index.ts +12 -5
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +23 -0
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/tips.txt +1 -0
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +1 -2
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +41 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +124 -119
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +169 -94
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +18 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/sdk.ts +11 -37
- package/src/session/agent-session.ts +82 -6
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +13 -5
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +212 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +50 -6
- package/src/tools/debug.ts +20 -8
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +44 -30
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +8 -1
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/plan-mode-guard.ts +21 -39
- package/src/tools/read.ts +23 -16
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +80 -78
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +81 -62
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +9 -1
- package/src/utils/enhanced-paste.ts +202 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
package/src/tools/ast-edit.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
@@ -11,26 +11,24 @@ import { normalizeToLF } from "../edit/normalize";
|
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
14
|
-
import { Ellipsis, fileHyperlink,
|
|
14
|
+
import { Ellipsis, fileHyperlink, framedBlock, renderStatusLine, truncateToWidth } from "../tui";
|
|
15
15
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
16
16
|
import type { ToolSession } from ".";
|
|
17
17
|
import { truncateForPrompt } from "./approval";
|
|
18
18
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
19
|
-
import { formatGroupedFiles } from "./grouped-file-output";
|
|
19
|
+
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
20
20
|
import type { OutputMeta } from "./output-meta";
|
|
21
21
|
import { isInternalUrlPath, resolveToolSearchScope } from "./path-utils";
|
|
22
22
|
import {
|
|
23
23
|
appendParseErrorsBulletList,
|
|
24
24
|
capParseErrors,
|
|
25
|
-
createCachedComponent,
|
|
26
25
|
formatCodeFrameLine,
|
|
27
26
|
formatCount,
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
formatErrorDetail,
|
|
28
|
+
formatMoreItems,
|
|
30
29
|
formatParseErrors,
|
|
31
30
|
formatParseErrorsCountLabel,
|
|
32
31
|
PREVIEW_LIMITS,
|
|
33
|
-
splitGroupsByBlankLine,
|
|
34
32
|
} from "./render-utils";
|
|
35
33
|
import { queueResolveHandler } from "./resolve";
|
|
36
34
|
import { ToolError } from "./tool-errors";
|
|
@@ -161,6 +159,9 @@ export interface AstEditToolDetails {
|
|
|
161
159
|
/** Absolute base directory used during the edit. Used by the renderer to resolve
|
|
162
160
|
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
163
161
|
searchPath?: string;
|
|
162
|
+
/** Session cwd at edit time. Display header paths are cwd-relative, so the
|
|
163
|
+
* renderer resolves them against this; `searchPath` is the scope target. */
|
|
164
|
+
cwd?: string;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
|
@@ -274,6 +275,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
274
275
|
...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
|
|
275
276
|
scopePath,
|
|
276
277
|
searchPath: resolvedSearchPath,
|
|
278
|
+
cwd: this.session.cwd,
|
|
277
279
|
files: fileList,
|
|
278
280
|
fileReplacements: [],
|
|
279
281
|
};
|
|
@@ -464,6 +466,32 @@ interface AstEditRenderArgs {
|
|
|
464
466
|
|
|
465
467
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
466
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Flatten pre-styled change groups into frame body lines. Groups are separated
|
|
471
|
+
* by a blank line and carry no tree guides — the frame border is the container,
|
|
472
|
+
* so nested `├─ │` gutters would just be noise. Collapsed mode always shows at
|
|
473
|
+
* least the first group, then fills up to `budget` lines before summarizing the
|
|
474
|
+
* rest as `… N more changes`.
|
|
475
|
+
*/
|
|
476
|
+
function buildChangeBody(groups: string[][], expanded: boolean, budget: number, theme: Theme): string[] {
|
|
477
|
+
const lines: string[] = [];
|
|
478
|
+
let shown = 0;
|
|
479
|
+
for (let i = 0; i < groups.length; i++) {
|
|
480
|
+
const group = groups[i]!;
|
|
481
|
+
const separator = shown > 0 ? 1 : 0;
|
|
482
|
+
const remainingAfter = groups.length - (i + 1);
|
|
483
|
+
const reserved = !expanded && remainingAfter > 0 ? 1 : 0;
|
|
484
|
+
// Always emit the first group; budget only gates subsequent ones.
|
|
485
|
+
if (!expanded && shown > 0 && lines.length + separator + group.length + reserved > budget) break;
|
|
486
|
+
if (separator) lines.push("");
|
|
487
|
+
lines.push(...group);
|
|
488
|
+
shown++;
|
|
489
|
+
}
|
|
490
|
+
const remaining = groups.length - shown;
|
|
491
|
+
if (!expanded && remaining > 0) lines.push(theme.fg("muted", formatMoreItems(remaining, "change")));
|
|
492
|
+
return lines;
|
|
493
|
+
}
|
|
494
|
+
|
|
467
495
|
export const astEditToolRenderer = {
|
|
468
496
|
inline: true,
|
|
469
497
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -473,8 +501,9 @@ export const astEditToolRenderer = {
|
|
|
473
501
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
474
502
|
|
|
475
503
|
const description = rewriteCount === 1 ? args.ops?.[0]?.pat : rewriteCount ? `${rewriteCount} rewrites` : "?";
|
|
476
|
-
const
|
|
477
|
-
|
|
504
|
+
const header = renderStatusLine({ icon: "pending", title: "AST Edit", description, meta }, uiTheme);
|
|
505
|
+
// Pending call has no body yet — a lone status line is sleeker than an empty frame.
|
|
506
|
+
return new Text(header, 0, 0);
|
|
478
507
|
},
|
|
479
508
|
|
|
480
509
|
renderResult(
|
|
@@ -487,7 +516,14 @@ export const astEditToolRenderer = {
|
|
|
487
516
|
|
|
488
517
|
if (result.isError) {
|
|
489
518
|
const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
|
|
490
|
-
|
|
519
|
+
const header = renderStatusLine({ icon: "error", title: "AST Edit" }, uiTheme);
|
|
520
|
+
return framedBlock(uiTheme, width => ({
|
|
521
|
+
header,
|
|
522
|
+
sections: [{ lines: formatErrorDetail(errorText, uiTheme).split("\n") }],
|
|
523
|
+
state: "error",
|
|
524
|
+
borderColor: "error",
|
|
525
|
+
width,
|
|
526
|
+
}));
|
|
491
527
|
}
|
|
492
528
|
|
|
493
529
|
const totalReplacements = details?.totalReplacements ?? 0;
|
|
@@ -502,9 +538,18 @@ export const astEditToolRenderer = {
|
|
|
502
538
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
503
539
|
if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
|
|
504
540
|
const header = renderStatusLine({ icon: "warning", title: "AST Edit", description, meta }, uiTheme);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
541
|
+
// The "0 replacements" count already rides on the status line; only parse
|
|
542
|
+
// errors are worth a body, so frame solely when there are some.
|
|
543
|
+
const bodyLines: string[] = [];
|
|
544
|
+
appendParseErrorsBulletList(bodyLines, details?.parseErrors, uiTheme, details?.parseErrorsTotal);
|
|
545
|
+
if (bodyLines.length === 0) return new Text(header, 0, 0);
|
|
546
|
+
return framedBlock(uiTheme, width => ({
|
|
547
|
+
header,
|
|
548
|
+
sections: [{ lines: bodyLines }],
|
|
549
|
+
state: "warning",
|
|
550
|
+
borderColor: "borderMuted",
|
|
551
|
+
width,
|
|
552
|
+
}));
|
|
508
553
|
}
|
|
509
554
|
|
|
510
555
|
const summaryParts = [formatCount("replacement", totalReplacements), formatCount("file", filesTouched)];
|
|
@@ -516,10 +561,33 @@ export const astEditToolRenderer = {
|
|
|
516
561
|
const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
|
|
517
562
|
|
|
518
563
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
);
|
|
564
|
+
const allLines = textContent.split("\n");
|
|
565
|
+
// Resolve hyperlinks over the whole output so nested directory headers
|
|
566
|
+
// reconstruct across the blank-line groups the tree list collapses by.
|
|
567
|
+
const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
|
|
568
|
+
const styledLines = allLines.map((line, index) => {
|
|
569
|
+
const ctx = contexts[index]!;
|
|
570
|
+
// Swap the inner code-frame gutter `│` for a space so it does not nest a
|
|
571
|
+
// second vertical bar inside the frame border.
|
|
572
|
+
const display = replaceTabs(line.replace("│", " "));
|
|
573
|
+
if (ctx.kind === "dir") {
|
|
574
|
+
const styled = uiTheme.fg("accent", display);
|
|
575
|
+
return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
|
|
576
|
+
}
|
|
577
|
+
if (ctx.kind === "file") {
|
|
578
|
+
const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", display);
|
|
579
|
+
return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
|
|
580
|
+
}
|
|
581
|
+
if (display.startsWith("+")) return uiTheme.fg("toolDiffAdded", display);
|
|
582
|
+
if (display.startsWith("-")) return uiTheme.fg("toolDiffRemoved", display);
|
|
583
|
+
return uiTheme.fg("toolOutput", display);
|
|
584
|
+
});
|
|
585
|
+
const changeGroups = groupLineIndicesByBlank(allLines)
|
|
586
|
+
.filter(indices => {
|
|
587
|
+
const first = allLines[indices[0]!]!;
|
|
588
|
+
return !first.startsWith("Safety cap reached") && !first.startsWith("Parse issues:");
|
|
589
|
+
})
|
|
590
|
+
.map(indices => indices.map(index => styledLines[index]!));
|
|
523
591
|
|
|
524
592
|
const badge = { label: "proposed", color: "warning" as const };
|
|
525
593
|
const header = renderStatusLine(
|
|
@@ -536,60 +604,19 @@ export const astEditToolRenderer = {
|
|
|
536
604
|
uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors, details.parseErrorsTotal)),
|
|
537
605
|
);
|
|
538
606
|
}
|
|
539
|
-
return
|
|
540
|
-
(
|
|
541
|
-
width
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return group.map(line => {
|
|
553
|
-
if (line.startsWith("## ")) {
|
|
554
|
-
// Strip ` (3 replacements)` and `#hash` suffixes from formatGroupedFiles.
|
|
555
|
-
const fileName = line
|
|
556
|
-
.slice(3)
|
|
557
|
-
.trimEnd()
|
|
558
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
559
|
-
.replace(/#[0-9a-f]+$/, "");
|
|
560
|
-
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
561
|
-
const styled = uiTheme.fg("dim", line);
|
|
562
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
563
|
-
}
|
|
564
|
-
if (line.startsWith("# ")) {
|
|
565
|
-
const raw = line
|
|
566
|
-
.slice(2)
|
|
567
|
-
.trimEnd()
|
|
568
|
-
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
569
|
-
const isDirectory = raw.endsWith("/");
|
|
570
|
-
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
571
|
-
if (isDirectory) {
|
|
572
|
-
if (searchBase) {
|
|
573
|
-
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
|
574
|
-
}
|
|
575
|
-
return uiTheme.fg("accent", line);
|
|
576
|
-
}
|
|
577
|
-
// Root-level file with optional `#hash` and ` (3 replacements)` suffixes.
|
|
578
|
-
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
579
|
-
const styled = uiTheme.fg("accent", line);
|
|
580
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
581
|
-
}
|
|
582
|
-
if (line.startsWith("+")) return uiTheme.fg("toolDiffAdded", line);
|
|
583
|
-
if (line.startsWith("-")) return uiTheme.fg("toolDiffRemoved", line);
|
|
584
|
-
return uiTheme.fg("toolOutput", line);
|
|
585
|
-
});
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
uiTheme,
|
|
589
|
-
);
|
|
590
|
-
return [header, ...changeLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
591
|
-
},
|
|
592
|
-
);
|
|
607
|
+
return framedBlock(uiTheme, width => {
|
|
608
|
+
const changeLines = buildChangeBody(changeGroups, Boolean(options.expanded), COLLAPSED_CHANGE_LIMIT, uiTheme);
|
|
609
|
+
const innerWidth = Math.max(1, width - 3);
|
|
610
|
+
const bodyLines = [...changeLines, ...extraLines].map(l => truncateToWidth(l, innerWidth, Ellipsis.Omit));
|
|
611
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
612
|
+
return {
|
|
613
|
+
header,
|
|
614
|
+
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
615
|
+
state: options.isPartial ? "pending" : "success",
|
|
616
|
+
borderColor: "borderMuted",
|
|
617
|
+
width,
|
|
618
|
+
};
|
|
619
|
+
});
|
|
593
620
|
},
|
|
594
621
|
mergeCallAndResult: true,
|
|
595
622
|
};
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWi
|
|
|
14
14
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
16
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
17
|
-
import { formatGroupedFiles } from "./grouped-file-output";
|
|
17
|
+
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
18
18
|
import { formatMatchLine } from "./match-line-format";
|
|
19
19
|
import type { OutputMeta } from "./output-meta";
|
|
20
20
|
import { resolveToolSearchScope } from "./path-utils";
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
formatParseErrors,
|
|
30
30
|
formatParseErrorsCountLabel,
|
|
31
31
|
PREVIEW_LIMITS,
|
|
32
|
-
splitGroupsByBlankLine,
|
|
33
32
|
} from "./render-utils";
|
|
34
33
|
import { ToolError } from "./tool-errors";
|
|
35
34
|
import { toolResult } from "./tool-result";
|
|
@@ -118,6 +117,9 @@ export interface AstGrepToolDetails {
|
|
|
118
117
|
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
119
118
|
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
120
119
|
searchPath?: string;
|
|
120
|
+
/** Session cwd at search time. Display header/match paths are cwd-relative, so
|
|
121
|
+
* the renderer resolves them against this; `searchPath` is the scope target. */
|
|
122
|
+
cwd?: string;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
|
|
@@ -207,6 +209,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
207
209
|
...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
|
|
208
210
|
scopePath,
|
|
209
211
|
searchPath: resolvedSearchPath,
|
|
212
|
+
cwd: this.session.cwd,
|
|
210
213
|
files: fileList,
|
|
211
214
|
fileMatches: [],
|
|
212
215
|
};
|
|
@@ -381,15 +384,41 @@ export const astGrepToolRenderer = {
|
|
|
381
384
|
if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
|
|
382
385
|
const description = args?.pat;
|
|
383
386
|
const header = renderStatusLine(
|
|
384
|
-
{
|
|
387
|
+
{
|
|
388
|
+
...(limitReached
|
|
389
|
+
? { icon: "warning" as const }
|
|
390
|
+
: { iconOverride: uiTheme.fg("accent", uiTheme.symbol("icon.search")) }),
|
|
391
|
+
title: "AST Grep",
|
|
392
|
+
description,
|
|
393
|
+
meta,
|
|
394
|
+
},
|
|
385
395
|
uiTheme,
|
|
386
396
|
);
|
|
387
397
|
|
|
388
398
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
399
|
+
const allLines = textContent.split("\n");
|
|
400
|
+
// Resolve hyperlinks over the whole output so nested directory headers
|
|
401
|
+
// reconstruct across the blank-line groups the tree list collapses by.
|
|
402
|
+
const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
|
|
403
|
+
const styledLines = allLines.map((line, index) => {
|
|
404
|
+
const ctx = contexts[index]!;
|
|
405
|
+
if (ctx.kind === "dir") {
|
|
406
|
+
const styled = uiTheme.fg("accent", line);
|
|
407
|
+
return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
|
|
408
|
+
}
|
|
409
|
+
if (ctx.kind === "file") {
|
|
410
|
+
const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
|
|
411
|
+
return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
|
|
412
|
+
}
|
|
413
|
+
if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
|
|
414
|
+
return uiTheme.fg("toolOutput", line);
|
|
415
|
+
});
|
|
416
|
+
const matchGroups = groupLineIndicesByBlank(allLines)
|
|
417
|
+
.filter(indices => {
|
|
418
|
+
const first = allLines[indices[0]!]!;
|
|
419
|
+
return !first.startsWith("Result limit reached") && !first.startsWith("Parse issues:");
|
|
420
|
+
})
|
|
421
|
+
.map(indices => indices.map(index => styledLines[index]!));
|
|
393
422
|
|
|
394
423
|
const extraLines: string[] = [];
|
|
395
424
|
if (limitReached) {
|
|
@@ -404,7 +433,6 @@ export const astGrepToolRenderer = {
|
|
|
404
433
|
return createCachedComponent(
|
|
405
434
|
() => options.expanded,
|
|
406
435
|
width => {
|
|
407
|
-
const searchBase = details?.searchPath;
|
|
408
436
|
const matchLines = renderTreeList(
|
|
409
437
|
{
|
|
410
438
|
items: matchGroups,
|
|
@@ -412,41 +440,7 @@ export const astGrepToolRenderer = {
|
|
|
412
440
|
maxCollapsed: matchGroups.length,
|
|
413
441
|
maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
|
|
414
442
|
itemType: "match",
|
|
415
|
-
renderItem: group =>
|
|
416
|
-
let contextDir = searchBase ?? "";
|
|
417
|
-
return group.map(line => {
|
|
418
|
-
if (line.startsWith("## ")) {
|
|
419
|
-
const fileName = line
|
|
420
|
-
.slice(3)
|
|
421
|
-
.trimEnd()
|
|
422
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
423
|
-
.replace(/#[0-9a-f]+$/, "");
|
|
424
|
-
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
425
|
-
const styled = uiTheme.fg("dim", line);
|
|
426
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
427
|
-
}
|
|
428
|
-
if (line.startsWith("# ")) {
|
|
429
|
-
const raw = line
|
|
430
|
-
.slice(2)
|
|
431
|
-
.trimEnd()
|
|
432
|
-
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
433
|
-
const isDirectory = raw.endsWith("/");
|
|
434
|
-
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
435
|
-
if (isDirectory) {
|
|
436
|
-
if (searchBase) {
|
|
437
|
-
contextDir = name === "." ? searchBase : path.join(searchBase, name);
|
|
438
|
-
}
|
|
439
|
-
return uiTheme.fg("accent", line);
|
|
440
|
-
}
|
|
441
|
-
// Root-level file (single # without trailing slash) from formatGroupedFiles.
|
|
442
|
-
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
443
|
-
const styled = uiTheme.fg("accent", line);
|
|
444
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
445
|
-
}
|
|
446
|
-
if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
|
|
447
|
-
return uiTheme.fg("toolOutput", line);
|
|
448
|
-
});
|
|
449
|
-
},
|
|
443
|
+
renderItem: group => group,
|
|
450
444
|
},
|
|
451
445
|
uiTheme,
|
|
452
446
|
);
|
package/src/tools/bash.ts
CHANGED
|
@@ -287,6 +287,35 @@ function formatExitCodeNotice(exitCode: number): string {
|
|
|
287
287
|
return `Command exited with code ${exitCode}`;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
const RAW_OUTPUT_ARTIFACT_PREFIX = "[raw output: artifact://";
|
|
291
|
+
const RAW_OUTPUT_ARTIFACT_SUFFIX = "]";
|
|
292
|
+
|
|
293
|
+
function stripRawOutputArtifactNotice(text: string): { text: string; artifactId?: string } {
|
|
294
|
+
const trimmed = text.trimEnd();
|
|
295
|
+
const lineStart = trimmed.lastIndexOf("\n");
|
|
296
|
+
const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
297
|
+
if (
|
|
298
|
+
!trimmed.startsWith(RAW_OUTPUT_ARTIFACT_PREFIX, candidateStart) ||
|
|
299
|
+
!trimmed.endsWith(RAW_OUTPUT_ARTIFACT_SUFFIX)
|
|
300
|
+
) {
|
|
301
|
+
return { text };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const idStart = candidateStart + RAW_OUTPUT_ARTIFACT_PREFIX.length;
|
|
305
|
+
const idEnd = trimmed.length - RAW_OUTPUT_ARTIFACT_SUFFIX.length;
|
|
306
|
+
if (idStart === idEnd) return { text };
|
|
307
|
+
for (let i = idStart; i < idEnd; i++) {
|
|
308
|
+
const code = trimmed.charCodeAt(i);
|
|
309
|
+
if (code < 48 || code > 57) return { text };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const artifactId = trimmed.slice(idStart, idEnd);
|
|
313
|
+
return {
|
|
314
|
+
text: trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd(),
|
|
315
|
+
artifactId,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
290
319
|
/**
|
|
291
320
|
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
292
321
|
* on each side) so the TUI can echo the value via a styled footer label
|
|
@@ -1028,6 +1057,7 @@ export interface ShellRendererConfig<TArgs> {
|
|
|
1028
1057
|
resolveCommand?: (args: TArgs | undefined) => string | undefined;
|
|
1029
1058
|
resolveCwd?: (args: TArgs | undefined) => string | undefined;
|
|
1030
1059
|
resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
|
|
1060
|
+
showHeader?: boolean;
|
|
1031
1061
|
}
|
|
1032
1062
|
|
|
1033
1063
|
function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
|
|
@@ -1079,9 +1109,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1079
1109
|
return {
|
|
1080
1110
|
renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1081
1111
|
const renderArgs = toBashRenderArgs(args, config);
|
|
1082
|
-
const title = config.resolveTitle(args, options);
|
|
1083
1112
|
const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
|
|
1084
|
-
const header =
|
|
1113
|
+
const header =
|
|
1114
|
+
config.showHeader === false
|
|
1115
|
+
? undefined
|
|
1116
|
+
: renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
|
|
1085
1117
|
const outputBlock = new CachedOutputBlock();
|
|
1086
1118
|
return markFramedBlockComponent({
|
|
1087
1119
|
render: (width: number): string[] =>
|
|
@@ -1115,8 +1147,10 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1115
1147
|
const cmdLines = args ? formatBashCommandLines(renderArgs, uiTheme) : undefined;
|
|
1116
1148
|
const isError = result.isError === true;
|
|
1117
1149
|
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1150
|
+
const header =
|
|
1151
|
+
config.showHeader === false
|
|
1152
|
+
? undefined
|
|
1153
|
+
: renderStatusLine({ icon, title: config.resolveTitle(args, options) }, uiTheme);
|
|
1120
1154
|
const details = result.details;
|
|
1121
1155
|
const outputBlock = new CachedOutputBlock();
|
|
1122
1156
|
|
|
@@ -1133,7 +1167,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1133
1167
|
const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1134
1168
|
const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
|
|
1135
1169
|
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1136
|
-
const
|
|
1170
|
+
const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
1171
|
+
const rawOutputArtifact = stripRawOutputArtifactNotice(withoutWall);
|
|
1172
|
+
const output = rawOutputArtifact.text;
|
|
1137
1173
|
const displayOutput = output.trimEnd();
|
|
1138
1174
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
1139
1175
|
|
|
@@ -1152,6 +1188,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1152
1188
|
: `Timeout: ${timeoutSeconds}s`,
|
|
1153
1189
|
);
|
|
1154
1190
|
}
|
|
1191
|
+
if (rawOutputArtifact.artifactId) {
|
|
1192
|
+
statsParts.push(`Artifact: ${rawOutputArtifact.artifactId}`);
|
|
1193
|
+
}
|
|
1155
1194
|
if (isError && typeof details?.exitCode === "number") {
|
|
1156
1195
|
statsParts.push(`Exit: ${details.exitCode}`);
|
|
1157
1196
|
}
|
|
@@ -1215,7 +1254,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1215
1254
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
1216
1255
|
],
|
|
1217
1256
|
width,
|
|
1218
|
-
animate:
|
|
1257
|
+
// Don't animate once the command has been backgrounded: the block
|
|
1258
|
+
// gets committed to scrollback and finalizes later via the async
|
|
1259
|
+
// update path, so a mid-sweep frame would freeze a stray dark
|
|
1260
|
+
// border segment.
|
|
1261
|
+
animate: options.isPartial && shimmerEnabled() && details?.async?.state !== "running",
|
|
1219
1262
|
},
|
|
1220
1263
|
uiTheme,
|
|
1221
1264
|
);
|
|
@@ -1235,4 +1278,5 @@ export const bashToolRenderer = createShellRenderer<BashRenderArgs>({
|
|
|
1235
1278
|
resolveCommand: args => args?.command,
|
|
1236
1279
|
resolveCwd: args => args?.cwd,
|
|
1237
1280
|
resolveEnv: args => args?.env,
|
|
1281
|
+
showHeader: false,
|
|
1238
1282
|
});
|
package/src/tools/debug.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type DapFunctionBreakpointRecord,
|
|
23
23
|
type DapInstructionBreakpointRecord,
|
|
24
24
|
type DapModule,
|
|
25
|
+
type DapResolvedAdapter,
|
|
25
26
|
type DapScope,
|
|
26
27
|
type DapSessionSummary,
|
|
27
28
|
type DapSource,
|
|
@@ -30,6 +31,8 @@ import {
|
|
|
30
31
|
type DapVariable,
|
|
31
32
|
dapSessionManager,
|
|
32
33
|
getAvailableAdapters,
|
|
34
|
+
type LaunchProgramKind,
|
|
35
|
+
resolveLaunchOverrides,
|
|
33
36
|
selectAttachAdapter,
|
|
34
37
|
selectLaunchAdapter,
|
|
35
38
|
} from "../dap";
|
|
@@ -489,16 +492,23 @@ function getConfiguredAdapters(cwd: string): string {
|
|
|
489
492
|
const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
|
|
490
493
|
return adapters.length > 0 ? adapters.join(", ") : "none";
|
|
491
494
|
}
|
|
492
|
-
|
|
493
|
-
|
|
495
|
+
|
|
496
|
+
async function classifyLaunchProgram(program: string): Promise<LaunchProgramKind> {
|
|
494
497
|
try {
|
|
495
|
-
|
|
498
|
+
return (await fs.stat(program)).isDirectory() ? "directory" : "file";
|
|
496
499
|
} catch (error) {
|
|
497
|
-
if (isEnoent(error)) return;
|
|
500
|
+
if (isEnoent(error)) return "missing";
|
|
498
501
|
throw error;
|
|
499
502
|
}
|
|
500
|
-
|
|
503
|
+
}
|
|
501
504
|
|
|
505
|
+
function validateLaunchProgram(
|
|
506
|
+
program: string,
|
|
507
|
+
cwd: string,
|
|
508
|
+
programKind: LaunchProgramKind,
|
|
509
|
+
adapter: DapResolvedAdapter,
|
|
510
|
+
): void {
|
|
511
|
+
if (programKind !== "directory" || adapter.acceptsDirectoryProgram) return;
|
|
502
512
|
const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
|
|
503
513
|
throw new ToolError(
|
|
504
514
|
`launch program resolves to a directory: ${displayPath}. Pass an executable file path, or for Python use adapter "debugpy" with program set to the .py file.`,
|
|
@@ -676,8 +686,8 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
676
686
|
}
|
|
677
687
|
const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
|
|
678
688
|
const program = resolveToCwd(params.program, commandCwd);
|
|
679
|
-
await
|
|
680
|
-
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter);
|
|
689
|
+
const programKind = await classifyLaunchProgram(program);
|
|
690
|
+
const adapter = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
|
|
681
691
|
if (!adapter) {
|
|
682
692
|
if (params.adapter === "debugpy") {
|
|
683
693
|
throw new ToolError("adapter 'debugpy' is not available: python not found in PATH");
|
|
@@ -686,8 +696,10 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
686
696
|
`No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
|
|
687
697
|
);
|
|
688
698
|
}
|
|
699
|
+
validateLaunchProgram(program, commandCwd, programKind, adapter);
|
|
700
|
+
const extraLaunchArguments = resolveLaunchOverrides(adapter, program, programKind);
|
|
689
701
|
const snapshot = await dapSessionManager.launch(
|
|
690
|
-
{ adapter, program, args: params.args, cwd: commandCwd },
|
|
702
|
+
{ adapter, program, args: params.args, cwd: commandCwd, extraLaunchArguments },
|
|
691
703
|
combinedSignal,
|
|
692
704
|
timeoutSec * 1000,
|
|
693
705
|
);
|