@oh-my-pi/pi-coding-agent 15.9.1 → 15.9.5
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 +68 -2
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -2
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +60 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -4
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +7 -1
- package/src/mcp/manager.ts +40 -21
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +102 -30
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +41 -3
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +99 -18
- package/src/session/agent-session.ts +103 -19
- package/src/session/auth-storage.ts +4 -0
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +3 -4
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +22 -8
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +78 -12
- package/src/tui/hyperlink.ts +42 -7
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
package/src/tools/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { AsyncJobManager } from "../async/job-manager";
|
|
5
6
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
6
7
|
import type { Settings } from "../config/settings";
|
|
7
8
|
import { EditTool } from "../edit";
|
|
@@ -22,6 +23,7 @@ import type { CustomMessage } from "../session/messages";
|
|
|
22
23
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
23
24
|
import { TaskTool } from "../task";
|
|
24
25
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
26
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
25
27
|
import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
|
|
26
28
|
import type { EventBus } from "../utils/event-bus";
|
|
27
29
|
import { WebSearchTool } from "../web/search";
|
|
@@ -183,6 +185,21 @@ export interface ToolSession {
|
|
|
183
185
|
modelRegistry?: import("../config/model-registry").ModelRegistry;
|
|
184
186
|
/** Agent output manager for unique agent:// IDs across task invocations */
|
|
185
187
|
agentOutputManager?: AgentOutputManager;
|
|
188
|
+
/**
|
|
189
|
+
* Async job manager scoped to this session.
|
|
190
|
+
*
|
|
191
|
+
* - Top-level session that constructed one: its own manager.
|
|
192
|
+
* - Subagent (`parentTaskPrefix` set): the parent's manager, so background
|
|
193
|
+
* bash/task work and `onJobComplete` deliveries flow into the conversation
|
|
194
|
+
* that spawned it.
|
|
195
|
+
* - Secondary in-process top-level session that found a singleton already
|
|
196
|
+
* installed (issue #1923): `undefined`. Tools refuse async work rather
|
|
197
|
+
* than silently route completions into the owning session's `yieldQueue`.
|
|
198
|
+
*
|
|
199
|
+
* Tools MUST use this instead of `AsyncJobManager.instance()` so a secondary
|
|
200
|
+
* session never borrows the owning session's manager by accident.
|
|
201
|
+
*/
|
|
202
|
+
asyncJobManager?: AsyncJobManager;
|
|
186
203
|
/** MCP manager visible to subagents without relying on the process-global singleton. */
|
|
187
204
|
mcpManager?: MCPManager;
|
|
188
205
|
/** Local protocol root to propagate to nested subagents and eval-created agents. */
|
|
@@ -404,14 +421,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
// Resolve effective tool discovery mode.
|
|
407
|
-
// tools.discoveryMode
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
: session.settings.get("mcp.discoveryMode")
|
|
413
|
-
? "mcp-only"
|
|
414
|
-
: "off";
|
|
424
|
+
// tools.discoveryMode controls the new modes; mcp.discoveryMode remains a back-compat alias for "mcp-only".
|
|
425
|
+
const effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
426
|
+
session.settings,
|
|
427
|
+
countToolsForAutoDiscovery(requestedTools ?? Object.keys(BUILTIN_TOOLS)),
|
|
428
|
+
);
|
|
415
429
|
const discoveryActive = effectiveDiscoveryMode !== "off";
|
|
416
430
|
|
|
417
431
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
package/src/tools/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
|
-
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
|
|
6
|
+
import { type AsyncJob, type AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
@@ -90,7 +90,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
90
90
|
onUpdate?: AgentToolUpdateCallback<JobToolDetails>,
|
|
91
91
|
_context?: AgentToolContext,
|
|
92
92
|
): Promise<AgentToolResult<JobToolDetails>> {
|
|
93
|
-
const manager =
|
|
93
|
+
const manager = this.session.asyncJobManager;
|
|
94
94
|
if (!manager) {
|
|
95
95
|
return {
|
|
96
96
|
content: [{ type: "text", text: "Async execution is disabled; no background jobs are available." }],
|
|
@@ -254,7 +254,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
254
254
|
): JobSnapshot[] {
|
|
255
255
|
const now = Date.now();
|
|
256
256
|
return jobs.map(j => {
|
|
257
|
-
const current =
|
|
257
|
+
const current = this.session.asyncJobManager?.getJob(j.id);
|
|
258
258
|
const latest = current ?? j;
|
|
259
259
|
return {
|
|
260
260
|
id: latest.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import * as z from "zod/v4";
|
|
4
|
-
import {
|
|
4
|
+
import { ensureBankExists } from "../hindsight/bank";
|
|
5
5
|
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
|
|
6
6
|
import type { ToolSession } from ".";
|
|
7
7
|
|
|
@@ -67,7 +67,7 @@ export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema>
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
try {
|
|
70
|
-
await
|
|
70
|
+
await ensureBankExists(state.client, state.bankId, state.config, state.banksSet);
|
|
71
71
|
const response = await state.client.reflect(state.bankId, params.query, {
|
|
72
72
|
context: params.context,
|
|
73
73
|
budget: state.config.recallBudget,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -217,6 +217,27 @@ export function parseLineRanges(sel: string): [LineRange, ...LineRange[]] | null
|
|
|
217
217
|
return merged as [LineRange, ...LineRange[]];
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Extract the line-range component from a read-tool selector that may also
|
|
222
|
+
* carry a verbatim/index display mode (`raw`, `conflicts`) — alone or compounded
|
|
223
|
+
* with a range (`raw:50-100`, `50-100:raw`). Returns the parsed ranges when the
|
|
224
|
+
* selector names any, otherwise `undefined` (pure `raw`/`conflicts`/none).
|
|
225
|
+
*
|
|
226
|
+
* Used by content search, which honors line ranges as a match filter but has no
|
|
227
|
+
* use for verbatim/conflict display modes — so those selectors are accepted and
|
|
228
|
+
* treated as an unfiltered, whole-resource search rather than rejected.
|
|
229
|
+
*/
|
|
230
|
+
export function selectorLineRanges(sel: string | undefined): [LineRange, ...LineRange[]] | undefined {
|
|
231
|
+
if (!sel) return undefined;
|
|
232
|
+
for (const chunk of sel.split(":")) {
|
|
233
|
+
const lower = chunk.toLowerCase();
|
|
234
|
+
if (lower === "raw" || lower === "conflicts") continue;
|
|
235
|
+
const ranges = parseLineRanges(chunk);
|
|
236
|
+
if (ranges) return ranges;
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
220
241
|
/** Return `true` when `lineNumber` (1-indexed) falls in any of the supplied ranges. */
|
|
221
242
|
export function isLineInRanges(lineNumber: number, ranges: readonly LineRange[]): boolean {
|
|
222
243
|
for (const range of ranges) {
|
package/src/tools/read.ts
CHANGED
|
@@ -2316,6 +2316,49 @@ interface ReadRenderArgs {
|
|
|
2316
2316
|
raw?: boolean;
|
|
2317
2317
|
}
|
|
2318
2318
|
|
|
2319
|
+
const INTERNAL_URL_LIKE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
2320
|
+
|
|
2321
|
+
function splitReadRenderPath(rawPath: string): { path: string; sel?: string } {
|
|
2322
|
+
if (INTERNAL_URL_LIKE_RE.test(rawPath)) {
|
|
2323
|
+
const internal = splitInternalUrlSel(rawPath);
|
|
2324
|
+
if (internal.sel) return internal;
|
|
2325
|
+
}
|
|
2326
|
+
return splitPathAndSel(rawPath);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function firstReadSelectorLine(sel: string | undefined): number | undefined {
|
|
2330
|
+
if (!sel) return undefined;
|
|
2331
|
+
try {
|
|
2332
|
+
const parsed = parseSel(sel);
|
|
2333
|
+
if (parsed.kind !== "lines") return undefined;
|
|
2334
|
+
return parsed.ranges[0].startLine;
|
|
2335
|
+
} catch {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function formatReadPathLink(
|
|
2341
|
+
rawPath: string,
|
|
2342
|
+
options: {
|
|
2343
|
+
resolvedPath?: string;
|
|
2344
|
+
suffixResolution?: { from: string; to: string };
|
|
2345
|
+
offset?: number;
|
|
2346
|
+
fallbackLabel?: string;
|
|
2347
|
+
},
|
|
2348
|
+
): string {
|
|
2349
|
+
const split = splitReadRenderPath(rawPath);
|
|
2350
|
+
const basePath = split.path || rawPath;
|
|
2351
|
+
const selectorSuffix = split.sel ? `:${split.sel}` : "";
|
|
2352
|
+
const plainDisplayPath = options.suffixResolution
|
|
2353
|
+
? shortenPath(options.suffixResolution.to)
|
|
2354
|
+
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2355
|
+
const target = options.resolvedPath ?? tryResolveInternalUrlSync(basePath);
|
|
2356
|
+
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2357
|
+
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2358
|
+
const displayPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
2359
|
+
return `${displayPath}${selectorSuffix}`;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2319
2362
|
export const readToolRenderer = {
|
|
2320
2363
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
2321
2364
|
if (isReadableUrlPath(args.file_path || args.path || "")) {
|
|
@@ -2323,13 +2366,10 @@ export const readToolRenderer = {
|
|
|
2323
2366
|
}
|
|
2324
2367
|
|
|
2325
2368
|
const rawPath = args.file_path || args.path || "";
|
|
2326
|
-
const shortPath = shortenPath(rawPath);
|
|
2327
|
-
const linkTarget = tryResolveInternalUrlSync(rawPath);
|
|
2328
|
-
const filePath = linkTarget ? fileHyperlink(linkTarget, shortPath) : shortPath;
|
|
2329
2369
|
const offset = args.offset;
|
|
2330
2370
|
const limit = args.limit;
|
|
2331
2371
|
|
|
2332
|
-
let pathDisplay =
|
|
2372
|
+
let pathDisplay = formatReadPathLink(rawPath, { offset, fallbackLabel: "…" }) || "…";
|
|
2333
2373
|
if (offset !== undefined || limit !== undefined) {
|
|
2334
2374
|
const startLine = offset ?? 1;
|
|
2335
2375
|
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
@@ -2363,7 +2403,7 @@ export const readToolRenderer = {
|
|
|
2363
2403
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
2364
2404
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
2365
2405
|
const rawPath = args?.file_path || args?.path || "";
|
|
2366
|
-
const filePath = shortenPath(rawPath);
|
|
2406
|
+
const filePath = formatReadPathLink(rawPath, { offset: args?.offset }) || shortenPath(rawPath);
|
|
2367
2407
|
let title = filePath ? `Read ${filePath}` : "Read";
|
|
2368
2408
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
2369
2409
|
const startLine = args.offset ?? 1;
|
|
@@ -2388,8 +2428,8 @@ export const readToolRenderer = {
|
|
|
2388
2428
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
2389
2429
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
2390
2430
|
const rawPath = args?.file_path || args?.path || "";
|
|
2391
|
-
const
|
|
2392
|
-
const lang = getLanguageFromPath(
|
|
2431
|
+
const renderPath = splitReadRenderPath(rawPath);
|
|
2432
|
+
const lang = getLanguageFromPath(renderPath.path);
|
|
2393
2433
|
|
|
2394
2434
|
const warningLines: string[] = [];
|
|
2395
2435
|
const truncation = details?.meta?.truncation;
|
|
@@ -2412,7 +2452,11 @@ export const readToolRenderer = {
|
|
|
2412
2452
|
|
|
2413
2453
|
if (imageContent) {
|
|
2414
2454
|
const suffix = details?.suffixResolution;
|
|
2415
|
-
const displayPath =
|
|
2455
|
+
const displayPath = formatReadPathLink(rawPath, {
|
|
2456
|
+
resolvedPath: details?.resolvedPath,
|
|
2457
|
+
suffixResolution: suffix,
|
|
2458
|
+
fallbackLabel: "image",
|
|
2459
|
+
});
|
|
2416
2460
|
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
2417
2461
|
const header = renderStatusLine(
|
|
2418
2462
|
{ icon: suffix ? "warning" : "success", title: "Read", description: `${displayPath}${correction}` },
|
|
@@ -2442,13 +2486,15 @@ export const readToolRenderer = {
|
|
|
2442
2486
|
}
|
|
2443
2487
|
|
|
2444
2488
|
const suffix = details?.suffixResolution;
|
|
2445
|
-
const plainDisplayPath = suffix ? shortenPath(suffix.to) : filePath;
|
|
2446
2489
|
// resolvedPath is the absolute fs path for fs-backed reads (regular files plus
|
|
2447
2490
|
// local:// / memory:// / skill:// / artifact:// resources). Fall back to a sync
|
|
2448
2491
|
// resolver for fs-backed internal URLs so the title is clickable even before the
|
|
2449
2492
|
// result lands or if the handler didn't populate resolvedPath.
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2493
|
+
const displayPath = formatReadPathLink(rawPath, {
|
|
2494
|
+
resolvedPath: details?.resolvedPath,
|
|
2495
|
+
suffixResolution: suffix,
|
|
2496
|
+
offset: args?.offset,
|
|
2497
|
+
});
|
|
2452
2498
|
const correction = suffix ? ` ${uiTheme.fg("dim", `(corrected from ${shortenPath(suffix.from)})`)}` : "";
|
|
2453
2499
|
let title = displayPath ? `Read ${displayPath}${correction}` : "Read";
|
|
2454
2500
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
@@ -2463,7 +2509,7 @@ export const readToolRenderer = {
|
|
|
2463
2509
|
const n = details.conflictCount;
|
|
2464
2510
|
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
2465
2511
|
}
|
|
2466
|
-
const rawRequested = args?.raw === true || isRawSelector(parseSel(
|
|
2512
|
+
const rawRequested = args?.raw === true || isRawSelector(parseSel(renderPath.sel));
|
|
2467
2513
|
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
2468
2514
|
let cachedWidth: number | undefined;
|
|
2469
2515
|
let cachedExpanded: boolean | undefined;
|
|
@@ -6,6 +6,7 @@ import * as z from "zod/v4";
|
|
|
6
6
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
7
7
|
import type { Theme } from "../modes/theme/theme";
|
|
8
8
|
import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
|
|
9
|
+
import { resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
9
10
|
import {
|
|
10
11
|
buildDiscoverableToolSearchIndex,
|
|
11
12
|
type DiscoverableTool,
|
|
@@ -198,12 +199,9 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
198
199
|
constructor(private readonly session: ToolSession) {}
|
|
199
200
|
|
|
200
201
|
static createIf(session: ToolSession): SearchToolBm25Tool | null {
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
(toolsDiscoveryMode !== undefined && toolsDiscoveryMode !== "off") ||
|
|
205
|
-
session.settings.get("mcp.discoveryMode") === true;
|
|
206
|
-
if (!active) return null;
|
|
202
|
+
// Direct createTools() calls do not know the final MCP/extension catalog yet, so
|
|
203
|
+
// auto mode is activated later by createAgentSession after the full registry exists.
|
|
204
|
+
if (resolveEffectiveToolDiscoveryMode(session.settings, 0) === "off") return null;
|
|
207
205
|
return supportsToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
|
|
208
206
|
}
|
|
209
207
|
|
package/src/tools/search.ts
CHANGED
|
@@ -16,7 +16,15 @@ import type { InternalResource, ResolveContext } from "../internal-urls/types";
|
|
|
16
16
|
import type { Theme } from "../modes/theme/theme";
|
|
17
17
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
18
18
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "../session/streaming-output";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
Ellipsis,
|
|
21
|
+
fileHyperlink,
|
|
22
|
+
renderStatusLine,
|
|
23
|
+
renderTreeList,
|
|
24
|
+
truncateToWidth,
|
|
25
|
+
tryResolveInternalUrlSync,
|
|
26
|
+
uriHyperlink,
|
|
27
|
+
} from "../tui";
|
|
20
28
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
21
29
|
import type { ToolSession } from ".";
|
|
22
30
|
import {
|
|
@@ -38,6 +46,8 @@ import {
|
|
|
38
46
|
type ResolvedSearchTarget,
|
|
39
47
|
resolveReadPath,
|
|
40
48
|
resolveToolSearchScope,
|
|
49
|
+
selectorLineRanges,
|
|
50
|
+
splitInternalUrlSel,
|
|
41
51
|
splitPathAndSel,
|
|
42
52
|
} from "./path-utils";
|
|
43
53
|
import {
|
|
@@ -109,6 +119,21 @@ interface SearchPathSpec {
|
|
|
109
119
|
function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
|
|
110
120
|
const specs: SearchPathSpec[] = [];
|
|
111
121
|
for (const entry of rawEntries) {
|
|
122
|
+
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
|
|
123
|
+
// which peels selector-shaped tails only for selector-capable schemes and
|
|
124
|
+
// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their
|
|
125
|
+
// verbatim/index display modes (`raw`, `conflicts`) carry no meaning for
|
|
126
|
+
// content search, so we accept them — searching the whole resource — and
|
|
127
|
+
// still honor any embedded line range as a match filter.
|
|
128
|
+
const internalSplit = splitInternalUrlSel(entry);
|
|
129
|
+
if (internalSplit.sel !== undefined) {
|
|
130
|
+
specs.push({
|
|
131
|
+
original: entry,
|
|
132
|
+
clean: internalSplit.path,
|
|
133
|
+
ranges: selectorLineRanges(internalSplit.sel),
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
112
137
|
const split = splitPathAndSel(entry);
|
|
113
138
|
let clean = entry;
|
|
114
139
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
@@ -259,7 +284,7 @@ interface IndexedContentLines {
|
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
262
|
-
const OMP_ROOT_URL_RE = /^omp
|
|
287
|
+
const OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
|
|
263
288
|
|
|
264
289
|
function normalizeSearchLine(line: string): string {
|
|
265
290
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
@@ -1145,6 +1170,26 @@ interface SearchRenderArgs {
|
|
|
1145
1170
|
|
|
1146
1171
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
1147
1172
|
|
|
1173
|
+
const SEARCH_CODE_FRAME_LINE_RE = /^\s*\*?(\d+)│/;
|
|
1174
|
+
|
|
1175
|
+
function searchScopeMeta(details: SearchToolDetails | undefined): string | undefined {
|
|
1176
|
+
if (!details?.scopePath) return undefined;
|
|
1177
|
+
const label = details.searchPath ? fileHyperlink(details.searchPath, details.scopePath) : details.scopePath;
|
|
1178
|
+
return `in ${label}`;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function linkUrlLikeSearchHeader(raw: string, styled: string): { line: string; absPath?: string } {
|
|
1182
|
+
const resolvedPath = tryResolveInternalUrlSync(raw);
|
|
1183
|
+
if (resolvedPath) return { line: fileHyperlink(resolvedPath, styled), absPath: resolvedPath };
|
|
1184
|
+
return { line: uriHyperlink(raw, styled) };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function parseSearchDisplayLineNumber(line: string): number | undefined {
|
|
1188
|
+
const match = SEARCH_CODE_FRAME_LINE_RE.exec(line);
|
|
1189
|
+
if (!match) return undefined;
|
|
1190
|
+
return Number.parseInt(match[1]!, 10);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1148
1193
|
export const searchToolRenderer = {
|
|
1149
1194
|
inline: true,
|
|
1150
1195
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -1220,8 +1265,11 @@ export const searchToolRenderer = {
|
|
|
1220
1265
|
: undefined;
|
|
1221
1266
|
|
|
1222
1267
|
if (matchCount === 0) {
|
|
1268
|
+
const meta = ["0 matches"];
|
|
1269
|
+
const scopeMeta = searchScopeMeta(details);
|
|
1270
|
+
if (scopeMeta) meta.push(scopeMeta);
|
|
1223
1271
|
const header = renderStatusLine(
|
|
1224
|
-
{ icon: "warning", title: "Search", description: args?.pattern, meta
|
|
1272
|
+
{ icon: "warning", title: "Search", description: args?.pattern, meta },
|
|
1225
1273
|
uiTheme,
|
|
1226
1274
|
);
|
|
1227
1275
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
@@ -1231,7 +1279,8 @@ export const searchToolRenderer = {
|
|
|
1231
1279
|
|
|
1232
1280
|
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
1233
1281
|
const meta = [...summaryParts];
|
|
1234
|
-
|
|
1282
|
+
const scopeMeta = searchScopeMeta(details);
|
|
1283
|
+
if (scopeMeta) meta.push(scopeMeta);
|
|
1235
1284
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
1236
1285
|
const description = args?.pattern ?? undefined;
|
|
1237
1286
|
const header = renderStatusLine(
|
|
@@ -1270,10 +1319,11 @@ export const searchToolRenderer = {
|
|
|
1270
1319
|
maxCollapsedLines: collapsedMatchLineBudget,
|
|
1271
1320
|
itemType: "match",
|
|
1272
1321
|
renderItem: group => {
|
|
1273
|
-
// Track directory context within a group
|
|
1274
|
-
//
|
|
1275
|
-
// from formatGroupedFiles (single-# when directory is `.`).
|
|
1322
|
+
// Track directory/file context within a group so headers and code-frame
|
|
1323
|
+
// lines link to the backing file, with line-specific links for matches.
|
|
1276
1324
|
let contextDir = searchBase ?? "";
|
|
1325
|
+
const hasFileHeader = group.some(line => line.startsWith("# "));
|
|
1326
|
+
let currentFilePath: string | undefined = hasFileHeader ? undefined : searchBase;
|
|
1277
1327
|
return group.map(line => {
|
|
1278
1328
|
if (line.startsWith("## ")) {
|
|
1279
1329
|
// Strip optional ` (suffix)` and `#hash` before resolving.
|
|
@@ -1283,6 +1333,7 @@ export const searchToolRenderer = {
|
|
|
1283
1333
|
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
1284
1334
|
.replace(/#[0-9a-f]+$/, "");
|
|
1285
1335
|
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
1336
|
+
currentFilePath = absPath;
|
|
1286
1337
|
const styled = uiTheme.fg("dim", line);
|
|
1287
1338
|
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1288
1339
|
}
|
|
@@ -1293,22 +1344,37 @@ export const searchToolRenderer = {
|
|
|
1293
1344
|
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
1294
1345
|
if (INTERNAL_URL_DISPLAY_RE.test(raw)) {
|
|
1295
1346
|
contextDir = "";
|
|
1296
|
-
|
|
1347
|
+
const styled = uiTheme.fg("accent", line);
|
|
1348
|
+
const linked = linkUrlLikeSearchHeader(raw, styled);
|
|
1349
|
+
currentFilePath = linked.absPath;
|
|
1350
|
+
return linked.line;
|
|
1297
1351
|
}
|
|
1298
1352
|
const isDirectory = raw.endsWith("/");
|
|
1299
1353
|
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
1300
1354
|
if (isDirectory) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1355
|
+
const absPath = searchBase
|
|
1356
|
+
? name === "."
|
|
1357
|
+
? searchBase
|
|
1358
|
+
: path.join(searchBase, name)
|
|
1359
|
+
: undefined;
|
|
1360
|
+
if (absPath) {
|
|
1361
|
+
contextDir = absPath;
|
|
1303
1362
|
}
|
|
1304
|
-
|
|
1363
|
+
currentFilePath = undefined;
|
|
1364
|
+
const styled = uiTheme.fg("accent", line);
|
|
1365
|
+
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1305
1366
|
}
|
|
1306
1367
|
// Root-level file emitted by formatGroupedFiles when the directory is `.`.
|
|
1307
1368
|
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
1369
|
+
currentFilePath = absPath;
|
|
1308
1370
|
const styled = uiTheme.fg("accent", line);
|
|
1309
1371
|
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1310
1372
|
}
|
|
1311
|
-
|
|
1373
|
+
const styled = uiTheme.fg("toolOutput", line);
|
|
1374
|
+
const lineNumber = parseSearchDisplayLineNumber(line);
|
|
1375
|
+
return currentFilePath && lineNumber !== undefined
|
|
1376
|
+
? fileHyperlink(currentFilePath, styled, { line: lineNumber })
|
|
1377
|
+
: styled;
|
|
1312
1378
|
});
|
|
1313
1379
|
},
|
|
1314
1380
|
},
|
package/src/tui/hyperlink.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OSC 8 terminal hyperlink support for
|
|
2
|
+
* OSC 8 terminal hyperlink support for paths and URLs.
|
|
3
3
|
*
|
|
4
4
|
* Wraps display text in `ESC ] 8 ; id=HASH ; URI ESC \ TEXT ESC ] 8 ; ; ESC \`
|
|
5
5
|
* sequences when the active terminal supports hyperlinks and the user setting
|
|
@@ -63,6 +63,46 @@ export function isHyperlinkEnabled(): boolean {
|
|
|
63
63
|
return TERMINAL.hyperlinks;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function safeHyperlinkUri(uri: string): string | undefined {
|
|
67
|
+
if (!uri || /[\x00-\x1f\x7f]/.test(uri)) return undefined;
|
|
68
|
+
return uri;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function wrapHyperlink(uri: string, displayText: string): string {
|
|
72
|
+
if (!isHyperlinkEnabled()) return displayText;
|
|
73
|
+
// Do not double-wrap if the text already embeds an OSC 8 sequence.
|
|
74
|
+
if (displayText.includes("\x1b]8;")) return displayText;
|
|
75
|
+
const safeUri = safeHyperlinkUri(uri);
|
|
76
|
+
if (!safeUri) return displayText;
|
|
77
|
+
const id = buildLinkId(safeUri);
|
|
78
|
+
return `${OSC}8;id=${id};${safeUri}${ST}${displayText}${OSC}8;;${ST}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at `uri`.
|
|
83
|
+
*
|
|
84
|
+
* Returns `displayText` unchanged when hyperlinks are disabled, `uri` contains
|
|
85
|
+
* terminal control bytes, or `displayText` already contains an OSC 8 sequence.
|
|
86
|
+
*/
|
|
87
|
+
export function uriHyperlink(uri: string, displayText: string): string {
|
|
88
|
+
return wrapHyperlink(uri, displayText);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL.
|
|
93
|
+
* `www.example.com` inputs are linked as `https://www.example.com`.
|
|
94
|
+
*/
|
|
95
|
+
export function urlHyperlink(url: string, displayText: string): string {
|
|
96
|
+
const normalized = url.match(/^www\./i) ? `https://${url}` : url;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = new URL(normalized);
|
|
99
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return displayText;
|
|
100
|
+
return wrapHyperlink(parsed.href, displayText);
|
|
101
|
+
} catch {
|
|
102
|
+
return displayText;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
66
106
|
/**
|
|
67
107
|
* Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
|
|
68
108
|
*
|
|
@@ -78,12 +118,7 @@ export function isHyperlinkEnabled(): boolean {
|
|
|
78
118
|
* @param opts - Optional line/col position appended as `?line=N&col=M` query params
|
|
79
119
|
*/
|
|
80
120
|
export function fileHyperlink(absPath: string, displayText: string, opts?: { line?: number; col?: number }): string {
|
|
81
|
-
|
|
82
|
-
// Do not double-wrap if the text already embeds an OSC 8 sequence.
|
|
83
|
-
if (displayText.includes("\x1b]8;")) return displayText;
|
|
84
|
-
const uri = buildFileUri(absPath, opts);
|
|
85
|
-
const id = buildLinkId(uri);
|
|
86
|
-
return `${OSC}8;id=${id};${uri}${ST}${displayText}${OSC}8;;${ST}`;
|
|
121
|
+
return wrapHyperlink(buildFileUri(absPath, opts), displayText);
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
/**
|
|
@@ -10,8 +10,6 @@ import path from "node:path";
|
|
|
10
10
|
import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@oh-my-pi/hashline";
|
|
11
11
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
13
|
-
import { glob } from "@oh-my-pi/pi-natives";
|
|
14
|
-
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
15
13
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
16
14
|
import { normalizeToLF } from "../edit/normalize";
|
|
17
15
|
import type { FileMentionMessage } from "../session/messages";
|
|
@@ -30,27 +28,6 @@ const LEADING_PUNCTUATION_REGEX = /^[`"'([{<]+/;
|
|
|
30
28
|
const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
|
|
31
29
|
const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
|
|
32
30
|
const DEFAULT_DIR_LIMIT = 500;
|
|
33
|
-
const MIN_FUZZY_QUERY_LENGTH = 5;
|
|
34
|
-
const MAX_RESOLUTION_CANDIDATES = 20_000;
|
|
35
|
-
const PATH_SEPARATOR_REGEX = /[/._\-\s]+/g;
|
|
36
|
-
|
|
37
|
-
type MentionDiscoveryProfile = {
|
|
38
|
-
hidden: boolean;
|
|
39
|
-
gitignore: boolean;
|
|
40
|
-
includeNodeModules: boolean;
|
|
41
|
-
maxResults: number;
|
|
42
|
-
cache: boolean;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function getMentionCandidateDiscoveryProfile(): MentionDiscoveryProfile {
|
|
46
|
-
return {
|
|
47
|
-
hidden: true,
|
|
48
|
-
gitignore: true,
|
|
49
|
-
cache: true,
|
|
50
|
-
includeNodeModules: true,
|
|
51
|
-
maxResults: MAX_RESOLUTION_CANDIDATES,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
31
|
|
|
55
32
|
// Avoid OOM when users @mention very large files. Above these limits we skip
|
|
56
33
|
// auto-reading and only include the path in the message.
|
|
@@ -70,16 +47,6 @@ function sanitizeMentionPath(rawPath: string): string | null {
|
|
|
70
47
|
return cleaned.length > 0 ? cleaned : null;
|
|
71
48
|
}
|
|
72
49
|
|
|
73
|
-
type MentionCandidate = {
|
|
74
|
-
path: string;
|
|
75
|
-
pathLower: string;
|
|
76
|
-
normalizedPath: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
function normalizeMentionQuery(query: string): string {
|
|
80
|
-
return query.toLowerCase().replace(PATH_SEPARATOR_REGEX, "");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
50
|
async function pathExists(filePath: string): Promise<boolean> {
|
|
84
51
|
try {
|
|
85
52
|
await Bun.file(filePath).stat();
|
|
@@ -89,75 +56,13 @@ async function pathExists(filePath: string): Promise<boolean> {
|
|
|
89
56
|
}
|
|
90
57
|
}
|
|
91
58
|
|
|
92
|
-
async function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pattern: "**/*",
|
|
98
|
-
path: cwd,
|
|
99
|
-
...discoveryProfile,
|
|
100
|
-
});
|
|
101
|
-
entries = result.matches.map(match => match.path);
|
|
102
|
-
} catch {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
107
|
-
const candidates: MentionCandidate[] = [];
|
|
108
|
-
for (const entry of entries) {
|
|
109
|
-
const pathLower = entry.toLowerCase();
|
|
110
|
-
const normalizedPath = normalizeMentionQuery(entry);
|
|
111
|
-
if (normalizedPath.length === 0) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
candidates.push({ path: entry, pathLower, normalizedPath });
|
|
115
|
-
}
|
|
116
|
-
return candidates;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function resolveMentionPath(
|
|
120
|
-
filePath: string,
|
|
121
|
-
cwd: string,
|
|
122
|
-
getMentionCandidates: () => Promise<MentionCandidate[]>,
|
|
123
|
-
): Promise<string | null> {
|
|
59
|
+
async function resolveMentionPath(filePath: string, cwd: string): Promise<string | null> {
|
|
60
|
+
// Exact resolution only. The TUI @-selector inserts the real, complete path, so a
|
|
61
|
+
// mention that does not resolve to an existing file or directory is prose, not a file
|
|
62
|
+
// reference. Fuzzy/prefix guessing here previously dragged in unrelated same-named
|
|
63
|
+
// files; that disambiguation belongs to the selector's display, not post-send.
|
|
124
64
|
const absolutePath = resolveReadPath(filePath, cwd);
|
|
125
|
-
|
|
126
|
-
return filePath;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const queryLower = filePath.toLowerCase();
|
|
130
|
-
const candidates = await getMentionCandidates();
|
|
131
|
-
const prefixMatches = candidates.filter(candidate => candidate.pathLower.startsWith(queryLower));
|
|
132
|
-
if (prefixMatches.length === 1) {
|
|
133
|
-
return prefixMatches[0]?.path ?? null;
|
|
134
|
-
}
|
|
135
|
-
if (prefixMatches.length > 1) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const normalizedQuery = normalizeMentionQuery(filePath);
|
|
140
|
-
if (normalizedQuery.length < MIN_FUZZY_QUERY_LENGTH) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const scored = candidates
|
|
145
|
-
.map(candidate => ({ candidate, match: fuzzyMatch(normalizedQuery, candidate.normalizedPath) }))
|
|
146
|
-
.filter(entry => entry.match.matches)
|
|
147
|
-
.sort((a, b) => {
|
|
148
|
-
if (a.match.score !== b.match.score) {
|
|
149
|
-
return a.match.score - b.match.score;
|
|
150
|
-
}
|
|
151
|
-
return a.candidate.path.localeCompare(b.candidate.path);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (scored.length === 0) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const best = scored[0];
|
|
159
|
-
|
|
160
|
-
return best?.candidate.path ?? null;
|
|
65
|
+
return (await pathExists(absolutePath)) ? filePath : null;
|
|
161
66
|
}
|
|
162
67
|
|
|
163
68
|
function buildTextOutput(textContent: string): { output: string; lineCount: number } {
|
|
@@ -285,14 +190,9 @@ export async function generateFileMentionMessages(
|
|
|
285
190
|
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
286
191
|
|
|
287
192
|
const files: FileMentionMessage["files"] = [];
|
|
288
|
-
let mentionCandidatesPromise: Promise<MentionCandidate[]> | null = null;
|
|
289
|
-
const getMentionCandidates = (): Promise<MentionCandidate[]> => {
|
|
290
|
-
mentionCandidatesPromise ??= listMentionCandidates(cwd);
|
|
291
|
-
return mentionCandidatesPromise;
|
|
292
|
-
};
|
|
293
193
|
|
|
294
194
|
for (const filePath of filePaths) {
|
|
295
|
-
const resolvedPath = await resolveMentionPath(filePath, cwd
|
|
195
|
+
const resolvedPath = await resolveMentionPath(filePath, cwd);
|
|
296
196
|
if (!resolvedPath) {
|
|
297
197
|
continue;
|
|
298
198
|
}
|