@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/read.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
34
34
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
35
35
|
import { convertFileWithMarkit } from "../utils/markit";
|
|
36
36
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
37
|
-
import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
37
|
+
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
38
38
|
import {
|
|
39
39
|
type ConflictEntry,
|
|
40
40
|
type ConflictScope,
|
|
@@ -1154,17 +1154,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1154
1154
|
const limitedEntries = listLimit.items;
|
|
1155
1155
|
const limitMeta = listLimit.meta;
|
|
1156
1156
|
|
|
1157
|
-
|
|
1158
|
-
for (const entry of limitedEntries) {
|
|
1157
|
+
for (let index = 0; index < limitedEntries.length; index++) {
|
|
1159
1158
|
throwIfAborted(signal);
|
|
1160
|
-
if (entry.isDirectory) {
|
|
1161
|
-
results.push(`${entry.name}/`);
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const sizeSuffix = entry.size > 0 ? ` (${formatBytes(entry.size)})` : "";
|
|
1166
|
-
results.push(`${entry.name}${sizeSuffix}`);
|
|
1167
1159
|
}
|
|
1160
|
+
const results = formatArchiveEntryLines(limitedEntries);
|
|
1168
1161
|
|
|
1169
1162
|
const output = results.length > 0 ? results.join("\n") : "(empty archive directory)";
|
|
1170
1163
|
const text = prependSuffixResolutionNotice(output, details.suffixResolution);
|
|
@@ -2337,10 +2330,20 @@ function firstReadSelectorLine(sel: string | undefined): number | undefined {
|
|
|
2337
2330
|
}
|
|
2338
2331
|
}
|
|
2339
2332
|
|
|
2333
|
+
/** Absolute fs path the read result actually resolved to, used as the OSC 8 link
|
|
2334
|
+
* target when the structured `resolvedPath` isn't set (the common plain-file and
|
|
2335
|
+
* image reads only record the path in `meta.source`). URL/internal sources are
|
|
2336
|
+
* not fs paths, so only `type: "path"` qualifies. */
|
|
2337
|
+
function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined {
|
|
2338
|
+
const source = details?.meta?.source;
|
|
2339
|
+
return source?.type === "path" ? source.value : undefined;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2340
2342
|
function formatReadPathLink(
|
|
2341
2343
|
rawPath: string,
|
|
2342
2344
|
options: {
|
|
2343
2345
|
resolvedPath?: string;
|
|
2346
|
+
sourcePath?: string;
|
|
2344
2347
|
suffixResolution?: { from: string; to: string };
|
|
2345
2348
|
offset?: number;
|
|
2346
2349
|
fallbackLabel?: string;
|
|
@@ -2352,7 +2355,7 @@ function formatReadPathLink(
|
|
|
2352
2355
|
const plainDisplayPath = options.suffixResolution
|
|
2353
2356
|
? shortenPath(options.suffixResolution.to)
|
|
2354
2357
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2355
|
-
const target = options.resolvedPath ?? tryResolveInternalUrlSync(basePath);
|
|
2358
|
+
const target = options.resolvedPath ?? options.sourcePath ?? tryResolveInternalUrlSync(basePath);
|
|
2356
2359
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2357
2360
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2358
2361
|
const displayPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
@@ -2403,7 +2406,9 @@ export const readToolRenderer = {
|
|
|
2403
2406
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
2404
2407
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
2405
2408
|
const rawPath = args?.file_path || args?.path || "";
|
|
2406
|
-
const filePath =
|
|
2409
|
+
const filePath =
|
|
2410
|
+
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
2411
|
+
shortenPath(rawPath);
|
|
2407
2412
|
let title = filePath ? `Read ${filePath}` : "Read";
|
|
2408
2413
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
2409
2414
|
const startLine = args.offset ?? 1;
|
|
@@ -2454,6 +2459,7 @@ export const readToolRenderer = {
|
|
|
2454
2459
|
const suffix = details?.suffixResolution;
|
|
2455
2460
|
const displayPath = formatReadPathLink(rawPath, {
|
|
2456
2461
|
resolvedPath: details?.resolvedPath,
|
|
2462
|
+
sourcePath: readSourceFsPath(details),
|
|
2457
2463
|
suffixResolution: suffix,
|
|
2458
2464
|
fallbackLabel: "image",
|
|
2459
2465
|
});
|
|
@@ -2486,12 +2492,13 @@ export const readToolRenderer = {
|
|
|
2486
2492
|
}
|
|
2487
2493
|
|
|
2488
2494
|
const suffix = details?.suffixResolution;
|
|
2489
|
-
// resolvedPath is the absolute fs path
|
|
2490
|
-
//
|
|
2491
|
-
//
|
|
2492
|
-
//
|
|
2495
|
+
// resolvedPath is the absolute fs path when a read resolved/corrected the
|
|
2496
|
+
// input (suffix match, internal URL, archive/sqlite/notebook); plain file
|
|
2497
|
+
// reads only record the absolute path in meta.source, so fall back to that
|
|
2498
|
+
// (and then to a sync internal-URL resolver) to keep the title clickable.
|
|
2493
2499
|
const displayPath = formatReadPathLink(rawPath, {
|
|
2494
2500
|
resolvedPath: details?.resolvedPath,
|
|
2501
|
+
sourcePath: readSourceFsPath(details),
|
|
2495
2502
|
suffixResolution: suffix,
|
|
2496
2503
|
offset: args?.offset,
|
|
2497
2504
|
});
|
|
@@ -10,8 +10,9 @@ import * as path from "node:path";
|
|
|
10
10
|
import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
12
12
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { getKeybindings, replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import { pluralize } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { formatKeyHints, type KeyId } from "../config/keybindings";
|
|
15
16
|
import { settings } from "../config/settings";
|
|
16
17
|
import type { Theme } from "../modes/theme/theme";
|
|
17
18
|
import { Hasher } from "../tui/utils";
|
|
@@ -75,8 +76,16 @@ export const TRUNCATE_LENGTHS = {
|
|
|
75
76
|
SHORT: 40,
|
|
76
77
|
} as const;
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
79
|
+
/** Keybinding action that toggles tool-output expansion. */
|
|
80
|
+
const EXPAND_ACTION = "app.tools.expand";
|
|
81
|
+
/** Fallback key when no binding is resolvable (e.g. outside an interactive session). */
|
|
82
|
+
const DEFAULT_EXPAND_KEY: KeyId = "ctrl+o";
|
|
83
|
+
|
|
84
|
+
/** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
|
|
85
|
+
export function expandKeyHint(): string {
|
|
86
|
+
const keys = getKeybindings().getKeys(EXPAND_ACTION);
|
|
87
|
+
return formatKeyHints(keys.length > 0 ? keys : [DEFAULT_EXPAND_KEY]);
|
|
88
|
+
}
|
|
80
89
|
|
|
81
90
|
// =============================================================================
|
|
82
91
|
// Text Truncation Utilities
|
|
@@ -150,7 +159,7 @@ export function formatStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFram
|
|
|
150
159
|
export function formatExpandHint(theme: Theme, expanded?: boolean, hasMore?: boolean): string {
|
|
151
160
|
if (expanded) return "";
|
|
152
161
|
if (hasMore === false) return "";
|
|
153
|
-
return theme.fg("dim", wrapBrackets(
|
|
162
|
+
return theme.fg("dim", wrapBrackets(`${expandKeyHint()}: Expand`, theme));
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
/**
|
|
@@ -736,36 +745,6 @@ export function capParseErrors(
|
|
|
736
745
|
// Renderer helpers shared by search / find / ast tools
|
|
737
746
|
// =============================================================================
|
|
738
747
|
|
|
739
|
-
/**
|
|
740
|
-
* Group `rawLines` by blank-line separators, mirroring the historical search /
|
|
741
|
-
* ast-grep / ast-edit renderer behavior: if any blank line is present, splits on
|
|
742
|
-
* runs of blank lines; otherwise collapses non-empty lines into a single group.
|
|
743
|
-
*/
|
|
744
|
-
export function splitGroupsByBlankLine(rawLines: string[]): string[][] {
|
|
745
|
-
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
746
|
-
const groups: string[][] = [];
|
|
747
|
-
if (hasSeparators) {
|
|
748
|
-
let current: string[] = [];
|
|
749
|
-
for (const line of rawLines) {
|
|
750
|
-
if (line.trim().length === 0) {
|
|
751
|
-
if (current.length > 0) {
|
|
752
|
-
groups.push(current);
|
|
753
|
-
current = [];
|
|
754
|
-
}
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
current.push(line);
|
|
758
|
-
}
|
|
759
|
-
if (current.length > 0) groups.push(current);
|
|
760
|
-
} else {
|
|
761
|
-
const nonEmpty = rawLines.filter(line => line.trim().length > 0);
|
|
762
|
-
if (nonEmpty.length > 0) {
|
|
763
|
-
groups.push(nonEmpty);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
return groups;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
748
|
/**
|
|
770
749
|
* Standard width+expand keyed render cache used by every search-style tool
|
|
771
750
|
* renderer. `compute` re-runs only when the cache key changes; the returned
|
|
@@ -774,6 +753,7 @@ export function splitGroupsByBlankLine(rawLines: string[]): string[][] {
|
|
|
774
753
|
export function createCachedComponent(
|
|
775
754
|
getExpanded: () => boolean,
|
|
776
755
|
compute: (width: number, expanded: boolean) => string[],
|
|
756
|
+
options: { paddingX?: number } = {},
|
|
777
757
|
): Component {
|
|
778
758
|
let cached: { key: bigint; lines: string[] } | undefined;
|
|
779
759
|
return {
|
|
@@ -781,9 +761,13 @@ export function createCachedComponent(
|
|
|
781
761
|
const expanded = getExpanded();
|
|
782
762
|
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
783
763
|
if (cached?.key === key) return cached.lines;
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
764
|
+
const paddingX = Math.max(0, options.paddingX ?? 0);
|
|
765
|
+
const innerWidth = Math.max(1, width - paddingX * 2);
|
|
766
|
+
const lines = compute(innerWidth, expanded);
|
|
767
|
+
const pad = paddingX === 0 ? "" : " ".repeat(paddingX);
|
|
768
|
+
const paddedLines = paddingX === 0 ? lines : lines.map(line => `${pad}${line}${pad}`);
|
|
769
|
+
cached = { key, lines: paddedLines };
|
|
770
|
+
return paddedLines;
|
|
787
771
|
},
|
|
788
772
|
invalidate() {
|
|
789
773
|
cached = undefined;
|
package/src/tools/resolve.ts
CHANGED
|
@@ -187,6 +187,20 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
|
|
|
187
187
|
return untilAborted(signal, async () => {
|
|
188
188
|
const invoker = this.session.peekQueueInvoker?.() ?? this.session.peekStandingResolveHandler?.();
|
|
189
189
|
if (!invoker) {
|
|
190
|
+
// `discard` is a request to cancel/abort a staged action. When nothing is
|
|
191
|
+
// pending, the desired end-state (no staged change) already holds, so honor
|
|
192
|
+
// it as a successful cancellation instead of surfacing a hard error to the
|
|
193
|
+
// model. `apply` still errors — there is nothing to apply.
|
|
194
|
+
if (params.action === "discard") {
|
|
195
|
+
return {
|
|
196
|
+
content: [{ type: "text" as const, text: "Nothing to discard; no pending action remains." }],
|
|
197
|
+
details: {
|
|
198
|
+
action: "discard",
|
|
199
|
+
reason: params.reason,
|
|
200
|
+
...(params.extra != null ? { extra: params.extra } : {}),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
190
204
|
throw new ToolError("No pending action to resolve. Nothing to apply or discard.");
|
|
191
205
|
}
|
|
192
206
|
const result = (await invoker(params)) as AgentToolResult<ResolveToolDetails>;
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
searchDiscoverableTools,
|
|
17
17
|
summarizeDiscoverableTools,
|
|
18
18
|
} from "../tool-discovery/tool-index";
|
|
19
|
-
import {
|
|
19
|
+
import { framedBlock, renderStatusLine, truncateToWidth } from "../tui";
|
|
20
20
|
import type { ToolSession } from ".";
|
|
21
|
-
import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
21
|
+
import { formatCount, formatExpandHint, formatMoreItems, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
22
22
|
import { ToolError } from "./tool-errors";
|
|
23
23
|
|
|
24
24
|
const DEFAULT_LIMIT = 8;
|
|
@@ -171,6 +171,25 @@ function renderMatchLines(match: SearchToolBm25Match, theme: Theme): string[] {
|
|
|
171
171
|
return lines;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function renderMatchBullets(tools: SearchToolBm25Match[], expanded: boolean, theme: Theme): string[] {
|
|
175
|
+
const shown = expanded ? tools.length : Math.min(tools.length, COLLAPSED_MATCH_LIMIT);
|
|
176
|
+
const bullet = theme.fg("dim", theme.format.bullet);
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
for (let i = 0; i < shown; i++) {
|
|
179
|
+
const itemLines = renderMatchLines(tools[i]!, theme);
|
|
180
|
+
lines.push(`${bullet} ${itemLines[0]}`);
|
|
181
|
+
for (let j = 1; j < itemLines.length; j++) {
|
|
182
|
+
lines.push(` ${itemLines[j]}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const remaining = tools.length - shown;
|
|
186
|
+
if (remaining > 0) {
|
|
187
|
+
const hint = formatExpandHint(theme, expanded, true);
|
|
188
|
+
lines.push(`${theme.fg("muted", formatMoreItems(remaining, "tool"))}${hint ? ` ${hint}` : ""}`);
|
|
189
|
+
}
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
|
|
174
193
|
function renderFallbackResult(text: string, theme: Theme): Component {
|
|
175
194
|
const header = renderStatusLine({ icon: "warning", title: TOOL_DISCOVERY_TITLE }, theme);
|
|
176
195
|
const bodyLines = (text || "Tool discovery completed")
|
|
@@ -271,14 +290,11 @@ export const searchToolBm25Renderer = {
|
|
|
271
290
|
renderCall(args: SearchToolBm25Params, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
272
291
|
const query = typeof args.query === "string" ? replaceTabs(args.query.trim()) : "";
|
|
273
292
|
const meta = args.limit ? [`limit:${args.limit}`] : [];
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
uiTheme,
|
|
278
|
-
),
|
|
279
|
-
0,
|
|
280
|
-
0,
|
|
293
|
+
const header = renderStatusLine(
|
|
294
|
+
{ icon: "pending", title: TOOL_DISCOVERY_TITLE, description: query || "(empty query)", meta },
|
|
295
|
+
uiTheme,
|
|
281
296
|
);
|
|
297
|
+
return new Text(header, 0, 0);
|
|
282
298
|
},
|
|
283
299
|
|
|
284
300
|
renderResult(
|
|
@@ -305,7 +321,9 @@ export const searchToolBm25Renderer = {
|
|
|
305
321
|
const safeQuery = replaceTabs(details.query);
|
|
306
322
|
const header = renderStatusLine(
|
|
307
323
|
{
|
|
308
|
-
|
|
324
|
+
...(details.tools.length > 0
|
|
325
|
+
? { iconOverride: uiTheme.fg("accent", uiTheme.symbol("icon.search")) }
|
|
326
|
+
: { icon: "warning" as const }),
|
|
309
327
|
title: TOOL_DISCOVERY_TITLE,
|
|
310
328
|
description: truncateToWidth(safeQuery, MATCH_LABEL_LEN),
|
|
311
329
|
meta,
|
|
@@ -318,19 +336,14 @@ export const searchToolBm25Renderer = {
|
|
|
318
336
|
return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
|
|
319
337
|
}
|
|
320
338
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
uiTheme,
|
|
331
|
-
);
|
|
332
|
-
lines.push(...treeLines);
|
|
333
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
339
|
+
return framedBlock(uiTheme, width => ({
|
|
340
|
+
header,
|
|
341
|
+
sections: [{ lines: renderMatchBullets(details.tools, options.expanded ?? false, uiTheme) }],
|
|
342
|
+
state: "success",
|
|
343
|
+
borderColor: "borderMuted",
|
|
344
|
+
applyBg: false,
|
|
345
|
+
width,
|
|
346
|
+
}));
|
|
334
347
|
},
|
|
335
348
|
|
|
336
349
|
mergeCallAndResult: true,
|
package/src/tools/search.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
parseArchivePathCandidates,
|
|
37
37
|
} from "./archive-reader";
|
|
38
38
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
39
|
-
import { formatGroupedFiles } from "./grouped-file-output";
|
|
39
|
+
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
40
40
|
import { formatMatchLine } from "./match-line-format";
|
|
41
41
|
import type { OutputMeta } from "./output-meta";
|
|
42
42
|
import {
|
|
@@ -61,7 +61,6 @@ import {
|
|
|
61
61
|
formatMoreItems,
|
|
62
62
|
PREVIEW_LIMITS,
|
|
63
63
|
replaceTabs,
|
|
64
|
-
splitGroupsByBlankLine,
|
|
65
64
|
} from "./render-utils";
|
|
66
65
|
import { ToolError } from "./tool-errors";
|
|
67
66
|
import { toolResult } from "./tool-result";
|
|
@@ -287,7 +286,6 @@ interface IndexedContentLines {
|
|
|
287
286
|
starts: number[];
|
|
288
287
|
}
|
|
289
288
|
|
|
290
|
-
const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
291
289
|
const OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
|
|
292
290
|
|
|
293
291
|
function normalizeSearchLine(line: string): string {
|
|
@@ -623,6 +621,10 @@ export interface SearchToolDetails {
|
|
|
623
621
|
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
624
622
|
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
625
623
|
searchPath?: string;
|
|
624
|
+
/** Session cwd at search time. The renderer resolves the display-relative
|
|
625
|
+
* (cwd-relative) header/match paths against this for OSC 8 hyperlinks;
|
|
626
|
+
* `searchPath` is the scope label target, not the display-path base. */
|
|
627
|
+
cwd?: string;
|
|
626
628
|
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
627
629
|
* skipped these and continued with the surviving entries; surfaced as a
|
|
628
630
|
* non-fatal warning in the renderer and in the model-facing text. */
|
|
@@ -1005,6 +1007,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1005
1007
|
const details: SearchToolDetails = {
|
|
1006
1008
|
scopePath,
|
|
1007
1009
|
searchPath,
|
|
1010
|
+
cwd: this.session.cwd,
|
|
1008
1011
|
matchCount: 0,
|
|
1009
1012
|
fileCount: 0,
|
|
1010
1013
|
files: [],
|
|
@@ -1131,6 +1134,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1131
1134
|
const details: SearchToolDetails = {
|
|
1132
1135
|
scopePath,
|
|
1133
1136
|
searchPath,
|
|
1137
|
+
cwd: this.session.cwd,
|
|
1134
1138
|
matchCount: selectedMatches.length,
|
|
1135
1139
|
fileCount: fileList.length,
|
|
1136
1140
|
files: fileList,
|
|
@@ -1210,69 +1214,49 @@ function isSearchMatchLine(line: string): boolean {
|
|
|
1210
1214
|
}
|
|
1211
1215
|
|
|
1212
1216
|
function isSearchHeaderLine(line: string): boolean {
|
|
1213
|
-
return
|
|
1217
|
+
return /^#+ /.test(line);
|
|
1214
1218
|
}
|
|
1215
1219
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1220
|
+
const URL_HEADER_PREFIX_RE = /^#+\s+/;
|
|
1221
|
+
|
|
1222
|
+
function renderSearchDisplayLines(
|
|
1223
|
+
lines: readonly string[],
|
|
1224
|
+
headerBase: string | undefined,
|
|
1225
|
+
fileScope: string | undefined,
|
|
1219
1226
|
uiTheme: Theme,
|
|
1220
1227
|
): RenderedSearchLine[] {
|
|
1221
|
-
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
.trimEnd()
|
|
1232
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
1233
|
-
.replace(/#[0-9a-f]+$/, "");
|
|
1234
|
-
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
1235
|
-
currentFilePath = absPath;
|
|
1236
|
-
const styled = uiTheme.fg("dim", line);
|
|
1237
|
-
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1228
|
+
const contexts = classifyGroupedLines(lines, headerBase, fileScope);
|
|
1229
|
+
// `classifyGroupedLines` can't resolve internal URLs (TUI-only), so track the
|
|
1230
|
+
// resolved URL target here and use it for the body lines that follow.
|
|
1231
|
+
let urlFile: string | undefined;
|
|
1232
|
+
return lines.map((line, index) => {
|
|
1233
|
+
const ctx = contexts[index]!;
|
|
1234
|
+
if (ctx.kind === "dir") {
|
|
1235
|
+
urlFile = undefined;
|
|
1236
|
+
const styled = uiTheme.fg("accent", line);
|
|
1237
|
+
return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
|
|
1238
1238
|
}
|
|
1239
|
-
if (
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const linked = linkUrlLikeSearchHeader(raw, styled);
|
|
1248
|
-
currentFilePath = linked.absPath;
|
|
1239
|
+
if (ctx.kind === "file") {
|
|
1240
|
+
if (ctx.isUrl) {
|
|
1241
|
+
const raw = line
|
|
1242
|
+
.replace(URL_HEADER_PREFIX_RE, "")
|
|
1243
|
+
.trimEnd()
|
|
1244
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
1245
|
+
const linked = linkUrlLikeSearchHeader(raw, uiTheme.fg("accent", line));
|
|
1246
|
+
urlFile = linked.absPath;
|
|
1249
1247
|
return { raw: line, styled: linked.line };
|
|
1250
1248
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
if (absPath) {
|
|
1256
|
-
contextDir = absPath;
|
|
1257
|
-
}
|
|
1258
|
-
currentFilePath = undefined;
|
|
1259
|
-
const styled = uiTheme.fg("accent", line);
|
|
1260
|
-
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1261
|
-
}
|
|
1262
|
-
// Root-level file emitted by formatGroupedFiles when the directory is `.`.
|
|
1263
|
-
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
1264
|
-
currentFilePath = absPath;
|
|
1265
|
-
const styled = uiTheme.fg("accent", line);
|
|
1266
|
-
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1249
|
+
urlFile = undefined;
|
|
1250
|
+
// Root-level files keep the bright accent; nested file headers are dimmed.
|
|
1251
|
+
const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
|
|
1252
|
+
return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
|
|
1267
1253
|
}
|
|
1268
1254
|
const styled = uiTheme.fg("toolOutput", line);
|
|
1269
1255
|
const lineNumber = parseSearchDisplayLineNumber(line);
|
|
1256
|
+
const filePath = ctx.filePath ?? urlFile;
|
|
1270
1257
|
return {
|
|
1271
1258
|
raw: line,
|
|
1272
|
-
styled:
|
|
1273
|
-
currentFilePath && lineNumber !== undefined
|
|
1274
|
-
? fileHyperlink(currentFilePath, styled, { line: lineNumber })
|
|
1275
|
-
: styled,
|
|
1259
|
+
styled: filePath && lineNumber !== undefined ? fileHyperlink(filePath, styled, { line: lineNumber }) : styled,
|
|
1276
1260
|
};
|
|
1277
1261
|
});
|
|
1278
1262
|
}
|
|
@@ -1288,19 +1272,15 @@ function countPreviewMatches(lines: readonly RenderedSearchLine[], hasMarkedMatc
|
|
|
1288
1272
|
}
|
|
1289
1273
|
|
|
1290
1274
|
function renderBudgetedSearchGroups(
|
|
1291
|
-
groups:
|
|
1275
|
+
groups: RenderedSearchLine[][],
|
|
1292
1276
|
maxLines: number,
|
|
1293
1277
|
matchCount: number,
|
|
1294
|
-
searchBase: string | undefined,
|
|
1295
1278
|
uiTheme: Theme,
|
|
1296
1279
|
compact: boolean,
|
|
1297
1280
|
): string[] {
|
|
1298
1281
|
if (maxLines <= 0) return [];
|
|
1299
1282
|
const renderedGroups = groups
|
|
1300
|
-
.map(group =>
|
|
1301
|
-
const rendered = renderSearchDisplayGroup(group, searchBase, uiTheme);
|
|
1302
|
-
return compact ? compactSearchPreviewGroup(rendered) : rendered;
|
|
1303
|
-
})
|
|
1283
|
+
.map(group => (compact ? compactSearchPreviewGroup(group) : group))
|
|
1304
1284
|
.filter(group => group.length > 0);
|
|
1305
1285
|
if (renderedGroups.length === 0) return [];
|
|
1306
1286
|
|
|
@@ -1352,6 +1332,10 @@ function renderBudgetedSearchGroups(
|
|
|
1352
1332
|
return lines;
|
|
1353
1333
|
}
|
|
1354
1334
|
|
|
1335
|
+
function searchStatusIcon(uiTheme: Theme): string {
|
|
1336
|
+
return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1355
1339
|
export const searchToolRenderer = {
|
|
1356
1340
|
inline: true,
|
|
1357
1341
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -1363,10 +1347,10 @@ export const searchToolRenderer = {
|
|
|
1363
1347
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
1364
1348
|
|
|
1365
1349
|
const text = renderStatusLine(
|
|
1366
|
-
{ icon: "pending", title: "Search", description: args.pattern || "?", meta },
|
|
1350
|
+
{ icon: "pending", title: "Search", titleColor: "toolTitle", description: args.pattern || "?", meta },
|
|
1367
1351
|
uiTheme,
|
|
1368
1352
|
);
|
|
1369
|
-
return new Text(text,
|
|
1353
|
+
return new Text(text, 1, 0);
|
|
1370
1354
|
},
|
|
1371
1355
|
|
|
1372
1356
|
renderResult(
|
|
@@ -1379,7 +1363,7 @@ export const searchToolRenderer = {
|
|
|
1379
1363
|
|
|
1380
1364
|
if (result.isError || details?.error) {
|
|
1381
1365
|
const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
|
|
1382
|
-
return new Text(formatErrorMessage(errorText, uiTheme),
|
|
1366
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
|
|
1383
1367
|
}
|
|
1384
1368
|
|
|
1385
1369
|
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
@@ -1387,12 +1371,18 @@ export const searchToolRenderer = {
|
|
|
1387
1371
|
if (!hasDetailedData) {
|
|
1388
1372
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text;
|
|
1389
1373
|
if (!textContent || textContent === "No matches found") {
|
|
1390
|
-
return new Text(formatEmptyMessage("No matches found", uiTheme),
|
|
1374
|
+
return new Text(formatEmptyMessage("No matches found", uiTheme), 1, 0);
|
|
1391
1375
|
}
|
|
1392
1376
|
const lines = textContent.split("\n").filter(line => line.trim() !== "");
|
|
1393
1377
|
const description = args?.pattern ?? undefined;
|
|
1394
1378
|
const header = renderStatusLine(
|
|
1395
|
-
{
|
|
1379
|
+
{
|
|
1380
|
+
iconOverride: searchStatusIcon(uiTheme),
|
|
1381
|
+
title: "Search",
|
|
1382
|
+
titleColor: "toolTitle",
|
|
1383
|
+
description,
|
|
1384
|
+
meta: [formatCount("item", lines.length)],
|
|
1385
|
+
},
|
|
1396
1386
|
uiTheme,
|
|
1397
1387
|
);
|
|
1398
1388
|
return createCachedComponent(
|
|
@@ -1411,6 +1401,7 @@ export const searchToolRenderer = {
|
|
|
1411
1401
|
);
|
|
1412
1402
|
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
1413
1403
|
},
|
|
1404
|
+
{ paddingX: 1 },
|
|
1414
1405
|
);
|
|
1415
1406
|
}
|
|
1416
1407
|
|
|
@@ -1431,12 +1422,12 @@ export const searchToolRenderer = {
|
|
|
1431
1422
|
const scopeMeta = searchScopeMeta(details);
|
|
1432
1423
|
if (scopeMeta) meta.push(scopeMeta);
|
|
1433
1424
|
const header = renderStatusLine(
|
|
1434
|
-
{ icon: "warning", title: "Search", description: args?.pattern, meta },
|
|
1425
|
+
{ icon: "warning", title: "Search", titleColor: "toolTitle", description: args?.pattern, meta },
|
|
1435
1426
|
uiTheme,
|
|
1436
1427
|
);
|
|
1437
1428
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
1438
1429
|
if (missingNote) lines.push(missingNote);
|
|
1439
|
-
return new Text(lines.join("\n"),
|
|
1430
|
+
return new Text(lines.join("\n"), 1, 0);
|
|
1440
1431
|
}
|
|
1441
1432
|
|
|
1442
1433
|
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
@@ -1446,12 +1437,30 @@ export const searchToolRenderer = {
|
|
|
1446
1437
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
1447
1438
|
const description = args?.pattern ?? undefined;
|
|
1448
1439
|
const header = renderStatusLine(
|
|
1449
|
-
{
|
|
1440
|
+
{
|
|
1441
|
+
...(truncated ? { icon: "warning" as const } : { iconOverride: searchStatusIcon(uiTheme) }),
|
|
1442
|
+
title: "Search",
|
|
1443
|
+
titleColor: "toolTitle",
|
|
1444
|
+
description,
|
|
1445
|
+
meta,
|
|
1446
|
+
},
|
|
1450
1447
|
uiTheme,
|
|
1451
1448
|
);
|
|
1452
1449
|
|
|
1453
1450
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1454
|
-
const
|
|
1451
|
+
const allLines = textContent.split("\n");
|
|
1452
|
+
// Resolve hyperlinks once over the whole output so a nested directory stack
|
|
1453
|
+
// reconstructs correctly across blank-line group boundaries.
|
|
1454
|
+
// Header/match display paths are cwd-relative, so resolve them against cwd
|
|
1455
|
+
// (falling back to searchPath for legacy results that predate `cwd`); the
|
|
1456
|
+
// scoped file's absolute path seeds body lines in single-file searches.
|
|
1457
|
+
const renderedLines = renderSearchDisplayLines(
|
|
1458
|
+
allLines,
|
|
1459
|
+
details?.cwd ?? details?.searchPath,
|
|
1460
|
+
details?.searchPath,
|
|
1461
|
+
uiTheme,
|
|
1462
|
+
);
|
|
1463
|
+
const matchGroups = groupLineIndicesByBlank(allLines).map(indices => indices.map(i => renderedLines[i]!));
|
|
1455
1464
|
|
|
1456
1465
|
const extraLines: string[] = [];
|
|
1457
1466
|
if (missingNote) extraLines.push(missingNote);
|
|
@@ -1463,17 +1472,10 @@ export const searchToolRenderer = {
|
|
|
1463
1472
|
(options.expanded ? EXPANDED_TEXT_LIMIT : COLLAPSED_TEXT_LIMIT) - extraLines.length,
|
|
1464
1473
|
0,
|
|
1465
1474
|
);
|
|
1466
|
-
const
|
|
1467
|
-
const matchLines = renderBudgetedSearchGroups(
|
|
1468
|
-
matchGroups,
|
|
1469
|
-
budget,
|
|
1470
|
-
matchCount,
|
|
1471
|
-
searchBase,
|
|
1472
|
-
uiTheme,
|
|
1473
|
-
!options.expanded,
|
|
1474
|
-
);
|
|
1475
|
+
const matchLines = renderBudgetedSearchGroups(matchGroups, budget, matchCount, uiTheme, !options.expanded);
|
|
1475
1476
|
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
1476
1477
|
},
|
|
1478
|
+
{ paddingX: 1 },
|
|
1477
1479
|
);
|
|
1478
1480
|
},
|
|
1479
1481
|
mergeCallAndResult: true,
|
|
@@ -5,6 +5,14 @@ import { ToolError } from "./tool-errors";
|
|
|
5
5
|
const SQLITE_MAGIC = new Uint8Array([
|
|
6
6
|
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
|
|
7
7
|
]);
|
|
8
|
+
|
|
9
|
+
export function looksLikeSqlite(bytes: Uint8Array): boolean {
|
|
10
|
+
if (bytes.byteLength < SQLITE_MAGIC.byteLength) return false;
|
|
11
|
+
for (const [index, byte] of SQLITE_MAGIC.entries()) {
|
|
12
|
+
if (bytes[index] !== byte) return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
8
16
|
const SQLITE_PATH_PATTERN = /\.(?:sqlite3?|db3?)(?=(?::|\?|$))/gi;
|
|
9
17
|
const DEFAULT_QUERY_LIMIT = 20;
|
|
10
18
|
const DEFAULT_SCHEMA_SAMPLE_LIMIT = 5;
|
|
@@ -443,18 +451,7 @@ export function parseSqlitePathCandidates(filePath: string): SqlitePathCandidate
|
|
|
443
451
|
|
|
444
452
|
export async function isSqliteFile(absolutePath: string): Promise<boolean> {
|
|
445
453
|
try {
|
|
446
|
-
|
|
447
|
-
if (bytes.length !== SQLITE_MAGIC.byteLength) {
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
for (const [index, byte] of SQLITE_MAGIC.entries()) {
|
|
452
|
-
if (bytes[index] !== byte) {
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return true;
|
|
454
|
+
return looksLikeSqlite(await Bun.file(absolutePath).slice(0, SQLITE_MAGIC.byteLength).bytes());
|
|
458
455
|
} catch {
|
|
459
456
|
return false;
|
|
460
457
|
}
|