@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- 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/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- 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/status-line.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 +17 -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 +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- 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/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -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/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- 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/path-utils.d.ts +8 -0
- 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 +6 -2
- 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/tools/yield.d.ts +8 -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/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -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 +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- 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 +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- 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 +164 -109
- 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/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- 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/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- 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 +2 -3
- 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 +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- 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 +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- 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/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -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 +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- 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 +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- 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 +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- 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
|
@@ -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";
|
|
@@ -84,6 +83,7 @@ const searchSchema = z
|
|
|
84
83
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
85
84
|
skip: z
|
|
86
85
|
.number()
|
|
86
|
+
.nullable()
|
|
87
87
|
.optional()
|
|
88
88
|
.describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
|
|
89
89
|
})
|
|
@@ -108,6 +108,10 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
108
108
|
* (DEFAULT_FILE_LIMIT files × MULTI_FILE_PER_FILE_MATCHES matches) plus
|
|
109
109
|
* pagination headroom so the caller can see total file count. */
|
|
110
110
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
111
|
+
/** Mirrors `MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`. Native grep
|
|
112
|
+
* silently returns no matches for files larger than this; surface a warning
|
|
113
|
+
* when the caller explicitly targeted such a file so they know to chunk it. */
|
|
114
|
+
const NATIVE_GREP_MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
117
|
* Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
|
|
@@ -287,7 +291,6 @@ interface IndexedContentLines {
|
|
|
287
291
|
starts: number[];
|
|
288
292
|
}
|
|
289
293
|
|
|
290
|
-
const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
291
294
|
const OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
|
|
292
295
|
|
|
293
296
|
function normalizeSearchLine(line: string): string {
|
|
@@ -623,6 +626,10 @@ export interface SearchToolDetails {
|
|
|
623
626
|
/** Absolute base directory used during search. Used by the renderer to resolve
|
|
624
627
|
* display-relative paths to absolute paths for OSC 8 hyperlinks. */
|
|
625
628
|
searchPath?: string;
|
|
629
|
+
/** Session cwd at search time. The renderer resolves the display-relative
|
|
630
|
+
* (cwd-relative) header/match paths against this for OSC 8 hyperlinks;
|
|
631
|
+
* `searchPath` is the scope label target, not the display-path base. */
|
|
632
|
+
cwd?: string;
|
|
626
633
|
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
627
634
|
* skipped these and continued with the surviving entries; surfaced as a
|
|
628
635
|
* non-fatal warning in the renderer and in the model-facing text. */
|
|
@@ -664,7 +671,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
664
671
|
throw new ToolError("Pattern must not be empty");
|
|
665
672
|
}
|
|
666
673
|
|
|
667
|
-
const normalizedSkip =
|
|
674
|
+
const normalizedSkip =
|
|
675
|
+
skip === undefined || skip === null ? 0 : Number.isFinite(skip) ? Math.floor(skip) : Number.NaN;
|
|
668
676
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
669
677
|
throw new ToolError("Skip must be a non-negative number");
|
|
670
678
|
}
|
|
@@ -726,7 +734,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
726
734
|
// reason instead of a downstream "path not found" from the scope resolver.
|
|
727
735
|
throw new ToolError(
|
|
728
736
|
`Cannot search archive member(s): ${archiveUnreadable.join(", ")}. ` +
|
|
729
|
-
`Read the
|
|
737
|
+
`Read the member with \`read <archive>:<member>\` and inspect the returned text, ` +
|
|
730
738
|
`or pass a UTF-8 text member.`,
|
|
731
739
|
);
|
|
732
740
|
}
|
|
@@ -989,6 +997,34 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
989
997
|
: "";
|
|
990
998
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
991
999
|
const fileMatchCounts = new Map<string, number>();
|
|
1000
|
+
// Detect explicit file targets that exceed the native grep size cap.
|
|
1001
|
+
// Native silently returns no matches above the cap; without this note the
|
|
1002
|
+
// caller sees "no matches" for a literal pattern that visibly exists.
|
|
1003
|
+
const oversizedNote = await (async (): Promise<string | undefined> => {
|
|
1004
|
+
const explicitFileTargets: string[] = [];
|
|
1005
|
+
if (exactFilePaths) {
|
|
1006
|
+
explicitFileTargets.push(...exactFilePaths);
|
|
1007
|
+
} else if (searchablePaths.length > 0 && !isDirectory && !multiTargets) {
|
|
1008
|
+
explicitFileTargets.push(searchPath);
|
|
1009
|
+
}
|
|
1010
|
+
if (explicitFileTargets.length === 0) return undefined;
|
|
1011
|
+
const oversized: string[] = [];
|
|
1012
|
+
await Promise.all(
|
|
1013
|
+
explicitFileTargets.map(async target => {
|
|
1014
|
+
try {
|
|
1015
|
+
const st = await stat(target);
|
|
1016
|
+
if (st.isFile() && st.size > NATIVE_GREP_MAX_FILE_BYTES) {
|
|
1017
|
+
oversized.push(path.relative(this.session.cwd, target) || target);
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
1020
|
+
// Stat failures here are surfaced by other code paths.
|
|
1021
|
+
}
|
|
1022
|
+
}),
|
|
1023
|
+
);
|
|
1024
|
+
if (oversized.length === 0) return undefined;
|
|
1025
|
+
const limitMb = Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024));
|
|
1026
|
+
return `Skipped oversized files (>${limitMb}MB grep limit; split the file or narrow with \`read\`): ${oversized.join(", ")}`;
|
|
1027
|
+
})();
|
|
992
1028
|
const archiveNote =
|
|
993
1029
|
archiveUnreadable.length > 0
|
|
994
1030
|
? `Skipped archive entries (search supports text members only): ${archiveUnreadable.join(", ")}`
|
|
@@ -1000,11 +1036,13 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1000
1036
|
const missingPathsNote =
|
|
1001
1037
|
missingPathsForNote.length > 0 ? `Skipped missing paths: ${missingPathsForNote.join(", ")}` : undefined;
|
|
1002
1038
|
const warningNote =
|
|
1003
|
-
[missingPathsNote, archiveNote].filter((s): s is string => Boolean(s)).join("\n") ||
|
|
1039
|
+
[missingPathsNote, archiveNote, oversizedNote].filter((s): s is string => Boolean(s)).join("\n") ||
|
|
1040
|
+
undefined;
|
|
1004
1041
|
if (selectedMatches.length === 0) {
|
|
1005
1042
|
const details: SearchToolDetails = {
|
|
1006
1043
|
scopePath,
|
|
1007
1044
|
searchPath,
|
|
1045
|
+
cwd: this.session.cwd,
|
|
1008
1046
|
matchCount: 0,
|
|
1009
1047
|
fileCount: 0,
|
|
1010
1048
|
files: [],
|
|
@@ -1131,6 +1169,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1131
1169
|
const details: SearchToolDetails = {
|
|
1132
1170
|
scopePath,
|
|
1133
1171
|
searchPath,
|
|
1172
|
+
cwd: this.session.cwd,
|
|
1134
1173
|
matchCount: selectedMatches.length,
|
|
1135
1174
|
fileCount: fileList.length,
|
|
1136
1175
|
files: fileList,
|
|
@@ -1210,69 +1249,49 @@ function isSearchMatchLine(line: string): boolean {
|
|
|
1210
1249
|
}
|
|
1211
1250
|
|
|
1212
1251
|
function isSearchHeaderLine(line: string): boolean {
|
|
1213
|
-
return
|
|
1252
|
+
return /^#+ /.test(line);
|
|
1214
1253
|
}
|
|
1215
1254
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1255
|
+
const URL_HEADER_PREFIX_RE = /^#+\s+/;
|
|
1256
|
+
|
|
1257
|
+
function renderSearchDisplayLines(
|
|
1258
|
+
lines: readonly string[],
|
|
1259
|
+
headerBase: string | undefined,
|
|
1260
|
+
fileScope: string | undefined,
|
|
1219
1261
|
uiTheme: Theme,
|
|
1220
1262
|
): 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 };
|
|
1263
|
+
const contexts = classifyGroupedLines(lines, headerBase, fileScope);
|
|
1264
|
+
// `classifyGroupedLines` can't resolve internal URLs (TUI-only), so track the
|
|
1265
|
+
// resolved URL target here and use it for the body lines that follow.
|
|
1266
|
+
let urlFile: string | undefined;
|
|
1267
|
+
return lines.map((line, index) => {
|
|
1268
|
+
const ctx = contexts[index]!;
|
|
1269
|
+
if (ctx.kind === "dir") {
|
|
1270
|
+
urlFile = undefined;
|
|
1271
|
+
const styled = uiTheme.fg("accent", line);
|
|
1272
|
+
return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
|
|
1238
1273
|
}
|
|
1239
|
-
if (
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const linked = linkUrlLikeSearchHeader(raw, styled);
|
|
1248
|
-
currentFilePath = linked.absPath;
|
|
1274
|
+
if (ctx.kind === "file") {
|
|
1275
|
+
if (ctx.isUrl) {
|
|
1276
|
+
const raw = line
|
|
1277
|
+
.replace(URL_HEADER_PREFIX_RE, "")
|
|
1278
|
+
.trimEnd()
|
|
1279
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
1280
|
+
const linked = linkUrlLikeSearchHeader(raw, uiTheme.fg("accent", line));
|
|
1281
|
+
urlFile = linked.absPath;
|
|
1249
1282
|
return { raw: line, styled: linked.line };
|
|
1250
1283
|
}
|
|
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 };
|
|
1284
|
+
urlFile = undefined;
|
|
1285
|
+
// Root-level files keep the bright accent; nested file headers are dimmed.
|
|
1286
|
+
const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
|
|
1287
|
+
return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
|
|
1267
1288
|
}
|
|
1268
1289
|
const styled = uiTheme.fg("toolOutput", line);
|
|
1269
1290
|
const lineNumber = parseSearchDisplayLineNumber(line);
|
|
1291
|
+
const filePath = ctx.filePath ?? urlFile;
|
|
1270
1292
|
return {
|
|
1271
1293
|
raw: line,
|
|
1272
|
-
styled:
|
|
1273
|
-
currentFilePath && lineNumber !== undefined
|
|
1274
|
-
? fileHyperlink(currentFilePath, styled, { line: lineNumber })
|
|
1275
|
-
: styled,
|
|
1294
|
+
styled: filePath && lineNumber !== undefined ? fileHyperlink(filePath, styled, { line: lineNumber }) : styled,
|
|
1276
1295
|
};
|
|
1277
1296
|
});
|
|
1278
1297
|
}
|
|
@@ -1288,19 +1307,15 @@ function countPreviewMatches(lines: readonly RenderedSearchLine[], hasMarkedMatc
|
|
|
1288
1307
|
}
|
|
1289
1308
|
|
|
1290
1309
|
function renderBudgetedSearchGroups(
|
|
1291
|
-
groups:
|
|
1310
|
+
groups: RenderedSearchLine[][],
|
|
1292
1311
|
maxLines: number,
|
|
1293
1312
|
matchCount: number,
|
|
1294
|
-
searchBase: string | undefined,
|
|
1295
1313
|
uiTheme: Theme,
|
|
1296
1314
|
compact: boolean,
|
|
1297
1315
|
): string[] {
|
|
1298
1316
|
if (maxLines <= 0) return [];
|
|
1299
1317
|
const renderedGroups = groups
|
|
1300
|
-
.map(group =>
|
|
1301
|
-
const rendered = renderSearchDisplayGroup(group, searchBase, uiTheme);
|
|
1302
|
-
return compact ? compactSearchPreviewGroup(rendered) : rendered;
|
|
1303
|
-
})
|
|
1318
|
+
.map(group => (compact ? compactSearchPreviewGroup(group) : group))
|
|
1304
1319
|
.filter(group => group.length > 0);
|
|
1305
1320
|
if (renderedGroups.length === 0) return [];
|
|
1306
1321
|
|
|
@@ -1352,6 +1367,10 @@ function renderBudgetedSearchGroups(
|
|
|
1352
1367
|
return lines;
|
|
1353
1368
|
}
|
|
1354
1369
|
|
|
1370
|
+
function searchStatusIcon(uiTheme: Theme): string {
|
|
1371
|
+
return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1355
1374
|
export const searchToolRenderer = {
|
|
1356
1375
|
inline: true,
|
|
1357
1376
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -1363,10 +1382,10 @@ export const searchToolRenderer = {
|
|
|
1363
1382
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
1364
1383
|
|
|
1365
1384
|
const text = renderStatusLine(
|
|
1366
|
-
{ icon: "pending", title: "Search", description: args.pattern || "?", meta },
|
|
1385
|
+
{ icon: "pending", title: "Search", titleColor: "toolTitle", description: args.pattern || "?", meta },
|
|
1367
1386
|
uiTheme,
|
|
1368
1387
|
);
|
|
1369
|
-
return new Text(text,
|
|
1388
|
+
return new Text(text, 1, 0);
|
|
1370
1389
|
},
|
|
1371
1390
|
|
|
1372
1391
|
renderResult(
|
|
@@ -1379,7 +1398,7 @@ export const searchToolRenderer = {
|
|
|
1379
1398
|
|
|
1380
1399
|
if (result.isError || details?.error) {
|
|
1381
1400
|
const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
|
|
1382
|
-
return new Text(formatErrorMessage(errorText, uiTheme),
|
|
1401
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
|
|
1383
1402
|
}
|
|
1384
1403
|
|
|
1385
1404
|
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
@@ -1387,12 +1406,18 @@ export const searchToolRenderer = {
|
|
|
1387
1406
|
if (!hasDetailedData) {
|
|
1388
1407
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text;
|
|
1389
1408
|
if (!textContent || textContent === "No matches found") {
|
|
1390
|
-
return new Text(formatEmptyMessage("No matches found", uiTheme),
|
|
1409
|
+
return new Text(formatEmptyMessage("No matches found", uiTheme), 1, 0);
|
|
1391
1410
|
}
|
|
1392
1411
|
const lines = textContent.split("\n").filter(line => line.trim() !== "");
|
|
1393
1412
|
const description = args?.pattern ?? undefined;
|
|
1394
1413
|
const header = renderStatusLine(
|
|
1395
|
-
{
|
|
1414
|
+
{
|
|
1415
|
+
iconOverride: searchStatusIcon(uiTheme),
|
|
1416
|
+
title: "Search",
|
|
1417
|
+
titleColor: "toolTitle",
|
|
1418
|
+
description,
|
|
1419
|
+
meta: [formatCount("item", lines.length)],
|
|
1420
|
+
},
|
|
1396
1421
|
uiTheme,
|
|
1397
1422
|
);
|
|
1398
1423
|
return createCachedComponent(
|
|
@@ -1411,6 +1436,7 @@ export const searchToolRenderer = {
|
|
|
1411
1436
|
);
|
|
1412
1437
|
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
1413
1438
|
},
|
|
1439
|
+
{ paddingX: 1 },
|
|
1414
1440
|
);
|
|
1415
1441
|
}
|
|
1416
1442
|
|
|
@@ -1431,12 +1457,12 @@ export const searchToolRenderer = {
|
|
|
1431
1457
|
const scopeMeta = searchScopeMeta(details);
|
|
1432
1458
|
if (scopeMeta) meta.push(scopeMeta);
|
|
1433
1459
|
const header = renderStatusLine(
|
|
1434
|
-
{ icon: "warning", title: "Search", description: args?.pattern, meta },
|
|
1460
|
+
{ icon: "warning", title: "Search", titleColor: "toolTitle", description: args?.pattern, meta },
|
|
1435
1461
|
uiTheme,
|
|
1436
1462
|
);
|
|
1437
1463
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
1438
1464
|
if (missingNote) lines.push(missingNote);
|
|
1439
|
-
return new Text(lines.join("\n"),
|
|
1465
|
+
return new Text(lines.join("\n"), 1, 0);
|
|
1440
1466
|
}
|
|
1441
1467
|
|
|
1442
1468
|
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
@@ -1446,12 +1472,30 @@ export const searchToolRenderer = {
|
|
|
1446
1472
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
1447
1473
|
const description = args?.pattern ?? undefined;
|
|
1448
1474
|
const header = renderStatusLine(
|
|
1449
|
-
{
|
|
1475
|
+
{
|
|
1476
|
+
...(truncated ? { icon: "warning" as const } : { iconOverride: searchStatusIcon(uiTheme) }),
|
|
1477
|
+
title: "Search",
|
|
1478
|
+
titleColor: "toolTitle",
|
|
1479
|
+
description,
|
|
1480
|
+
meta,
|
|
1481
|
+
},
|
|
1450
1482
|
uiTheme,
|
|
1451
1483
|
);
|
|
1452
1484
|
|
|
1453
1485
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1454
|
-
const
|
|
1486
|
+
const allLines = textContent.split("\n");
|
|
1487
|
+
// Resolve hyperlinks once over the whole output so a nested directory stack
|
|
1488
|
+
// reconstructs correctly across blank-line group boundaries.
|
|
1489
|
+
// Header/match display paths are cwd-relative, so resolve them against cwd
|
|
1490
|
+
// (falling back to searchPath for legacy results that predate `cwd`); the
|
|
1491
|
+
// scoped file's absolute path seeds body lines in single-file searches.
|
|
1492
|
+
const renderedLines = renderSearchDisplayLines(
|
|
1493
|
+
allLines,
|
|
1494
|
+
details?.cwd ?? details?.searchPath,
|
|
1495
|
+
details?.searchPath,
|
|
1496
|
+
uiTheme,
|
|
1497
|
+
);
|
|
1498
|
+
const matchGroups = groupLineIndicesByBlank(allLines).map(indices => indices.map(i => renderedLines[i]!));
|
|
1455
1499
|
|
|
1456
1500
|
const extraLines: string[] = [];
|
|
1457
1501
|
if (missingNote) extraLines.push(missingNote);
|
|
@@ -1463,17 +1507,10 @@ export const searchToolRenderer = {
|
|
|
1463
1507
|
(options.expanded ? EXPANDED_TEXT_LIMIT : COLLAPSED_TEXT_LIMIT) - extraLines.length,
|
|
1464
1508
|
0,
|
|
1465
1509
|
);
|
|
1466
|
-
const
|
|
1467
|
-
const matchLines = renderBudgetedSearchGroups(
|
|
1468
|
-
matchGroups,
|
|
1469
|
-
budget,
|
|
1470
|
-
matchCount,
|
|
1471
|
-
searchBase,
|
|
1472
|
-
uiTheme,
|
|
1473
|
-
!options.expanded,
|
|
1474
|
-
);
|
|
1510
|
+
const matchLines = renderBudgetedSearchGroups(matchGroups, budget, matchCount, uiTheme, !options.expanded);
|
|
1475
1511
|
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
1476
1512
|
},
|
|
1513
|
+
{ paddingX: 1 },
|
|
1477
1514
|
);
|
|
1478
1515
|
},
|
|
1479
1516
|
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
|
}
|