@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/fetch.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
2
4
|
import * as path from "node:path";
|
|
3
5
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
4
6
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
@@ -8,6 +10,7 @@ import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
|
8
10
|
import { parseHTML } from "linkedom";
|
|
9
11
|
import { LRUCache } from "lru-cache/raw";
|
|
10
12
|
import type { Settings } from "../config/settings";
|
|
13
|
+
import { readEditableNotebookText } from "../edit/notebook";
|
|
11
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
15
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
13
16
|
import type { ToolSession } from "../sdk";
|
|
@@ -22,10 +25,12 @@ import { specialHandlers } from "../web/scrapers";
|
|
|
22
25
|
import type { RenderResult } from "../web/scrapers/types";
|
|
23
26
|
import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
24
27
|
import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
|
|
28
|
+
import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "./archive-reader";
|
|
25
29
|
import { applyListLimit } from "./list-limit";
|
|
26
30
|
import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
|
|
27
31
|
import { type LineRange, parseLineRanges } from "./path-utils";
|
|
28
|
-
import { formatExpandHint, getDomain, replaceTabs } from "./render-utils";
|
|
32
|
+
import { formatBytes, formatExpandHint, getDomain, replaceTabs } from "./render-utils";
|
|
33
|
+
import { listTables, looksLikeSqlite, renderTableList } from "./sqlite-reader";
|
|
29
34
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
30
35
|
import { toolResult } from "./tool-result";
|
|
31
36
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -46,8 +51,6 @@ const CONVERTIBLE_MIMES = new Set([
|
|
|
46
51
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
47
52
|
"application/rtf",
|
|
48
53
|
"application/epub+zip",
|
|
49
|
-
"application/x-ipynb+json",
|
|
50
|
-
"application/zip",
|
|
51
54
|
"image/png",
|
|
52
55
|
"image/jpeg",
|
|
53
56
|
"image/gif",
|
|
@@ -67,7 +70,6 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
67
70
|
".xlsx",
|
|
68
71
|
".rtf",
|
|
69
72
|
".epub",
|
|
70
|
-
".ipynb",
|
|
71
73
|
".png",
|
|
72
74
|
".jpg",
|
|
73
75
|
".jpeg",
|
|
@@ -78,6 +80,27 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
78
80
|
".ogg",
|
|
79
81
|
]);
|
|
80
82
|
|
|
83
|
+
const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
|
|
84
|
+
const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
|
|
85
|
+
|
|
86
|
+
const SQLITE_MIMES = new Set([
|
|
87
|
+
"application/vnd.sqlite3",
|
|
88
|
+
"application/x-sqlite3",
|
|
89
|
+
"application/sqlite3",
|
|
90
|
+
"application/sqlite",
|
|
91
|
+
]);
|
|
92
|
+
const SQLITE_EXTENSIONS = new Set([".sqlite", ".sqlite3", ".db", ".db3"]);
|
|
93
|
+
|
|
94
|
+
const ARCHIVE_MIMES = new Set([
|
|
95
|
+
"application/zip",
|
|
96
|
+
"application/x-zip-compressed",
|
|
97
|
+
"application/x-tar",
|
|
98
|
+
"application/tar",
|
|
99
|
+
"application/gzip",
|
|
100
|
+
"application/x-gzip",
|
|
101
|
+
]);
|
|
102
|
+
const ARCHIVE_EXTENSIONS = new Set([".zip", ".tar", ".tar.gz", ".tgz", ".gz"]);
|
|
103
|
+
|
|
81
104
|
const IMAGE_MIME_BY_EXTENSION = new Map<string, string>([
|
|
82
105
|
[".png", "image/png"],
|
|
83
106
|
[".jpg", "image/jpeg"],
|
|
@@ -261,6 +284,12 @@ function normalizeMime(contentType: string): string {
|
|
|
261
284
|
return contentType.split(";")[0].trim().toLowerCase();
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
function getFilenameExtensionHint(filename: string): string {
|
|
288
|
+
const lower = filename.toLowerCase();
|
|
289
|
+
if (lower.endsWith(".tar.gz")) return ".tar.gz";
|
|
290
|
+
return path.extname(filename).toLowerCase();
|
|
291
|
+
}
|
|
292
|
+
|
|
264
293
|
/**
|
|
265
294
|
* Get extension from URL or Content-Disposition
|
|
266
295
|
*/
|
|
@@ -269,7 +298,7 @@ function getExtensionHint(url: string, contentDisposition?: string): string {
|
|
|
269
298
|
if (contentDisposition) {
|
|
270
299
|
const match = contentDisposition.match(/filename[*]?=["']?([^"';\n]+)/i);
|
|
271
300
|
if (match) {
|
|
272
|
-
const ext =
|
|
301
|
+
const ext = getFilenameExtensionHint(match[1]);
|
|
273
302
|
if (ext) return ext;
|
|
274
303
|
}
|
|
275
304
|
}
|
|
@@ -277,7 +306,7 @@ function getExtensionHint(url: string, contentDisposition?: string): string {
|
|
|
277
306
|
// Fall back to URL path
|
|
278
307
|
try {
|
|
279
308
|
const pathname = new URL(url).pathname;
|
|
280
|
-
const ext =
|
|
309
|
+
const ext = getFilenameExtensionHint(pathname);
|
|
281
310
|
if (ext) return ext;
|
|
282
311
|
} catch {}
|
|
283
312
|
|
|
@@ -738,6 +767,254 @@ type FetchRenderResult = RenderResult & {
|
|
|
738
767
|
image?: FetchImagePayload;
|
|
739
768
|
};
|
|
740
769
|
|
|
770
|
+
const BINARY_SAMPLE_CHARS = 4096;
|
|
771
|
+
const URL_ARCHIVE_LIST_LIMIT = 500;
|
|
772
|
+
const URL_SQLITE_LIST_LIMIT = 500;
|
|
773
|
+
|
|
774
|
+
function sampleLooksBinary(text: string): boolean {
|
|
775
|
+
const limit = Math.min(text.length, BINARY_SAMPLE_CHARS);
|
|
776
|
+
if (limit === 0) return false;
|
|
777
|
+
|
|
778
|
+
let replacementCount = 0;
|
|
779
|
+
for (let index = 0; index < limit; index++) {
|
|
780
|
+
const code = text.charCodeAt(index);
|
|
781
|
+
if (code === 0) return true;
|
|
782
|
+
if (code === 0xfffd) replacementCount++;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return replacementCount >= 3 && replacementCount / limit > 0.01;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function isNotebookHint(mime: string, extensionHint: string): boolean {
|
|
789
|
+
return NOTEBOOK_MIMES.has(mime) || NOTEBOOK_EXTENSIONS.has(extensionHint);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function isSqliteHint(mime: string, extensionHint: string): boolean {
|
|
793
|
+
return SQLITE_MIMES.has(mime) || SQLITE_EXTENSIONS.has(extensionHint);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function isArchiveHint(mime: string, extensionHint: string): boolean {
|
|
797
|
+
return ARCHIVE_MIMES.has(mime) || ARCHIVE_EXTENSIONS.has(extensionHint);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getArchiveFormatHint(mime: string, extensionHint: string): ArchiveFormat | undefined {
|
|
801
|
+
if (extensionHint === ".zip" || mime === "application/zip" || mime === "application/x-zip-compressed") {
|
|
802
|
+
return "zip";
|
|
803
|
+
}
|
|
804
|
+
if (extensionHint === ".tar" || mime === "application/x-tar" || mime === "application/tar") {
|
|
805
|
+
return "tar";
|
|
806
|
+
}
|
|
807
|
+
if (
|
|
808
|
+
extensionHint === ".tar.gz" ||
|
|
809
|
+
extensionHint === ".tgz" ||
|
|
810
|
+
extensionHint === ".gz" ||
|
|
811
|
+
mime === "application/gzip" ||
|
|
812
|
+
mime === "application/x-gzip"
|
|
813
|
+
) {
|
|
814
|
+
return "tar.gz";
|
|
815
|
+
}
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function formatErrorMessage(error: unknown): string {
|
|
820
|
+
return error instanceof Error ? error.message : String(error);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function binaryContentType(mime: string): string {
|
|
824
|
+
return mime || "application/octet-stream";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function buildBinaryNotice(finalUrl: string, mime: string, byteLength?: number): string {
|
|
828
|
+
const size = byteLength === undefined ? "unknown size" : formatBytes(byteLength);
|
|
829
|
+
return `[Binary content: ${binaryContentType(mime)}, ${size}] ${finalUrl}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function buildBinaryPayloadResult(
|
|
833
|
+
url: string,
|
|
834
|
+
finalUrl: string,
|
|
835
|
+
mime: string,
|
|
836
|
+
method: string,
|
|
837
|
+
content: string,
|
|
838
|
+
fetchedAt: string,
|
|
839
|
+
notes: string[],
|
|
840
|
+
): FetchRenderResult {
|
|
841
|
+
const output = finalizeOutput(content);
|
|
842
|
+
return {
|
|
843
|
+
url,
|
|
844
|
+
finalUrl,
|
|
845
|
+
contentType: binaryContentType(mime),
|
|
846
|
+
method,
|
|
847
|
+
content: output.content,
|
|
848
|
+
fetchedAt,
|
|
849
|
+
truncated: output.truncated,
|
|
850
|
+
notes,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function withTempBinaryFile<T>(
|
|
855
|
+
prefix: string,
|
|
856
|
+
extension: string,
|
|
857
|
+
bytes: Uint8Array,
|
|
858
|
+
readTempFile: (tempPath: string) => Promise<T>,
|
|
859
|
+
): Promise<T> {
|
|
860
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
861
|
+
const tempPath = path.join(tempDir, `payload${extension}`);
|
|
862
|
+
try {
|
|
863
|
+
await Bun.write(tempPath, bytes);
|
|
864
|
+
return await readTempFile(tempPath);
|
|
865
|
+
} finally {
|
|
866
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function renderNotebookPayload(bytes: Uint8Array, displayUrl: string): Promise<string> {
|
|
871
|
+
return withTempBinaryFile("omp-url-notebook-", ".ipynb", bytes, tempPath =>
|
|
872
|
+
readEditableNotebookText(tempPath, displayUrl),
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function renderSqlitePayload(bytes: Uint8Array): Promise<string> {
|
|
877
|
+
return withTempBinaryFile("omp-url-sqlite-", ".sqlite", bytes, async tempPath => {
|
|
878
|
+
let db: Database | null = null;
|
|
879
|
+
try {
|
|
880
|
+
db = new Database(tempPath, { readonly: true, strict: true });
|
|
881
|
+
db.run("PRAGMA busy_timeout = 3000");
|
|
882
|
+
const listLimit = applyListLimit(listTables(db), { limit: URL_SQLITE_LIST_LIMIT });
|
|
883
|
+
return renderTableList(listLimit.items);
|
|
884
|
+
} finally {
|
|
885
|
+
db?.close();
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function tryRenderBinaryPayload(
|
|
891
|
+
url: string,
|
|
892
|
+
finalUrl: string,
|
|
893
|
+
mime: string,
|
|
894
|
+
extHint: string,
|
|
895
|
+
rawContent: string,
|
|
896
|
+
timeout: number,
|
|
897
|
+
signal: AbortSignal | undefined,
|
|
898
|
+
fetchedAt: string,
|
|
899
|
+
notes: readonly string[],
|
|
900
|
+
): Promise<FetchRenderResult | null> {
|
|
901
|
+
const hasNotebookHint = isNotebookHint(mime, extHint);
|
|
902
|
+
const hasSqliteHint = isSqliteHint(mime, extHint);
|
|
903
|
+
const hasArchiveHint = isArchiveHint(mime, extHint);
|
|
904
|
+
const rawLooksBinary = sampleLooksBinary(rawContent);
|
|
905
|
+
if (!hasNotebookHint && !hasSqliteHint && !hasArchiveHint && !rawLooksBinary) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const resultNotes = [...notes];
|
|
910
|
+
const binary = await fetchBinary(finalUrl, timeout, signal);
|
|
911
|
+
if (!binary.ok) {
|
|
912
|
+
resultNotes.push(binary.error ? `Binary fetch failed: ${binary.error}` : "Binary fetch failed");
|
|
913
|
+
return buildBinaryPayloadResult(
|
|
914
|
+
url,
|
|
915
|
+
finalUrl,
|
|
916
|
+
mime,
|
|
917
|
+
"binary",
|
|
918
|
+
buildBinaryNotice(finalUrl, mime),
|
|
919
|
+
fetchedAt,
|
|
920
|
+
resultNotes,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const binaryExtHint = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
925
|
+
if (isNotebookHint(mime, binaryExtHint)) {
|
|
926
|
+
try {
|
|
927
|
+
return buildBinaryPayloadResult(
|
|
928
|
+
url,
|
|
929
|
+
finalUrl,
|
|
930
|
+
mime,
|
|
931
|
+
"notebook",
|
|
932
|
+
await renderNotebookPayload(binary.buffer, finalUrl),
|
|
933
|
+
fetchedAt,
|
|
934
|
+
resultNotes,
|
|
935
|
+
);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
resultNotes.push(`Notebook rendering failed: ${formatErrorMessage(error)}`);
|
|
938
|
+
return buildBinaryPayloadResult(
|
|
939
|
+
url,
|
|
940
|
+
finalUrl,
|
|
941
|
+
mime,
|
|
942
|
+
"binary",
|
|
943
|
+
buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
|
|
944
|
+
fetchedAt,
|
|
945
|
+
resultNotes,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (isSqliteHint(mime, binaryExtHint) || looksLikeSqlite(binary.buffer)) {
|
|
951
|
+
try {
|
|
952
|
+
return buildBinaryPayloadResult(
|
|
953
|
+
url,
|
|
954
|
+
finalUrl,
|
|
955
|
+
mime,
|
|
956
|
+
"sqlite",
|
|
957
|
+
await renderSqlitePayload(binary.buffer),
|
|
958
|
+
fetchedAt,
|
|
959
|
+
resultNotes,
|
|
960
|
+
);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
resultNotes.push(`SQLite rendering failed: ${formatErrorMessage(error)}`);
|
|
963
|
+
return buildBinaryPayloadResult(
|
|
964
|
+
url,
|
|
965
|
+
finalUrl,
|
|
966
|
+
mime,
|
|
967
|
+
"binary",
|
|
968
|
+
buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
|
|
969
|
+
fetchedAt,
|
|
970
|
+
resultNotes,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const hintedArchiveFormat = getArchiveFormatHint(mime, binaryExtHint);
|
|
976
|
+
const shouldArchiveSniff = hintedArchiveFormat !== undefined || !isConvertible(mime, binaryExtHint);
|
|
977
|
+
const archiveFormat = hintedArchiveFormat ?? (shouldArchiveSniff ? sniffArchiveFormat(binary.buffer) : undefined);
|
|
978
|
+
if (archiveFormat) {
|
|
979
|
+
try {
|
|
980
|
+
return buildBinaryPayloadResult(
|
|
981
|
+
url,
|
|
982
|
+
finalUrl,
|
|
983
|
+
mime,
|
|
984
|
+
"archive",
|
|
985
|
+
await listArchiveRoot(binary.buffer, archiveFormat, { limit: URL_ARCHIVE_LIST_LIMIT }),
|
|
986
|
+
fetchedAt,
|
|
987
|
+
resultNotes,
|
|
988
|
+
);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
resultNotes.push(`Archive rendering failed: ${formatErrorMessage(error)}`);
|
|
991
|
+
return buildBinaryPayloadResult(
|
|
992
|
+
url,
|
|
993
|
+
finalUrl,
|
|
994
|
+
mime,
|
|
995
|
+
"binary",
|
|
996
|
+
buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
|
|
997
|
+
fetchedAt,
|
|
998
|
+
resultNotes,
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (rawLooksBinary) {
|
|
1004
|
+
return buildBinaryPayloadResult(
|
|
1005
|
+
url,
|
|
1006
|
+
finalUrl,
|
|
1007
|
+
mime,
|
|
1008
|
+
"binary",
|
|
1009
|
+
buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
|
|
1010
|
+
fetchedAt,
|
|
1011
|
+
resultNotes,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
741
1018
|
// =============================================================================
|
|
742
1019
|
// Unified Special Handler Dispatch
|
|
743
1020
|
// =============================================================================
|
|
@@ -984,6 +1261,19 @@ async function renderUrl(
|
|
|
984
1261
|
}
|
|
985
1262
|
}
|
|
986
1263
|
|
|
1264
|
+
const binaryPayloadResult = await tryRenderBinaryPayload(
|
|
1265
|
+
url,
|
|
1266
|
+
finalUrl,
|
|
1267
|
+
mime,
|
|
1268
|
+
extHint,
|
|
1269
|
+
rawContent,
|
|
1270
|
+
timeout,
|
|
1271
|
+
signal,
|
|
1272
|
+
fetchedAt,
|
|
1273
|
+
notes,
|
|
1274
|
+
);
|
|
1275
|
+
if (binaryPayloadResult) return binaryPayloadResult;
|
|
1276
|
+
|
|
987
1277
|
// Step 4: Handle non-HTML text content
|
|
988
1278
|
const isHtml = mime.includes("html") || mime.includes("xhtml");
|
|
989
1279
|
const isJson = mime.includes("json");
|
|
@@ -992,7 +1282,7 @@ async function renderUrl(
|
|
|
992
1282
|
const isFeed = mime.includes("rss") || mime.includes("atom") || mime.includes("feed");
|
|
993
1283
|
|
|
994
1284
|
// Raw mode skips every text-shaping branch below (JSON pretty-print, feed-to-markdown,
|
|
995
|
-
// HTML extraction) and returns the response body verbatim.
|
|
1285
|
+
// HTML extraction) and returns the response body verbatim. Binary-oriented branches
|
|
996
1286
|
// above already ran because raw isn't useful for binary payloads.
|
|
997
1287
|
if (raw) {
|
|
998
1288
|
const output = finalizeOutput(rawContent);
|
package/src/tools/find.ts
CHANGED
|
@@ -13,6 +13,7 @@ import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
|
13
13
|
import { type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
14
14
|
import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
|
+
import { buildPathTree, walkPathTree } from "./grouped-file-output";
|
|
16
17
|
import { applyListLimit } from "./list-limit";
|
|
17
18
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
18
19
|
import {
|
|
@@ -54,34 +55,27 @@ const MIN_GLOB_TIMEOUT_MS = 500;
|
|
|
54
55
|
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* Group find matches
|
|
58
|
-
* tokens for shared path prefixes.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Group find matches into a multi-level directory tree so the model doesn't pay
|
|
59
|
+
* repeated tokens for shared path prefixes. Single-child directory chains fold
|
|
60
|
+
* into one header (`# a/b/c/`), so a common prefix — including an absolute root
|
|
61
|
+
* for out-of-cwd results — collapses to a single line. Each level adds one `#`;
|
|
62
|
+
* files are listed bare under the deepest directory header that owns them.
|
|
63
|
+
*
|
|
64
|
+
* Order follows the input (mtime-desc for native glob): a directory appears when
|
|
65
|
+
* its first member is emitted, and a node's own files precede its subdirectories.
|
|
61
66
|
*/
|
|
62
67
|
export function formatFindGroupedOutput(paths: readonly string[]): string {
|
|
63
68
|
if (paths.length === 0) return "";
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const dir = slash === -1 ? "" : trimmed.slice(0, slash);
|
|
70
|
-
const base = slash === -1 ? trimmed : trimmed.slice(slash + 1);
|
|
71
|
-
const label = hasTrailingSlash ? `${base}/` : base;
|
|
72
|
-
const list = groups.get(dir);
|
|
73
|
-
if (list) list.push(label);
|
|
74
|
-
else groups.set(dir, [label]);
|
|
75
|
-
}
|
|
76
|
-
const sections: string[] = [];
|
|
77
|
-
for (const [dir, entries] of groups) {
|
|
78
|
-
if (dir === "") {
|
|
79
|
-
sections.push(entries.join("\n"));
|
|
69
|
+
const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
|
|
70
|
+
const lines: string[] = [];
|
|
71
|
+
for (const event of walkPathTree(tree)) {
|
|
72
|
+
if (event.kind === "dir") {
|
|
73
|
+
lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
|
|
80
74
|
} else {
|
|
81
|
-
|
|
75
|
+
lines.push(event.name);
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
|
-
return
|
|
78
|
+
return lines.join("\n");
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
export interface FindToolDetails {
|
|
@@ -434,6 +428,10 @@ function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefin
|
|
|
434
428
|
|
|
435
429
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
436
430
|
|
|
431
|
+
function findStatusIcon(uiTheme: Theme): string {
|
|
432
|
+
return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
|
|
433
|
+
}
|
|
434
|
+
|
|
437
435
|
export const findToolRenderer = {
|
|
438
436
|
inline: true,
|
|
439
437
|
renderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -441,10 +439,16 @@ export const findToolRenderer = {
|
|
|
441
439
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
442
440
|
|
|
443
441
|
const text = renderStatusLine(
|
|
444
|
-
{
|
|
442
|
+
{
|
|
443
|
+
icon: "pending",
|
|
444
|
+
title: "Find",
|
|
445
|
+
titleColor: "toolTitle",
|
|
446
|
+
description: formatFindRenderPaths(args.paths) || "*",
|
|
447
|
+
meta,
|
|
448
|
+
},
|
|
445
449
|
uiTheme,
|
|
446
450
|
);
|
|
447
|
-
return new Text(text,
|
|
451
|
+
return new Text(text, 1, 0);
|
|
448
452
|
},
|
|
449
453
|
|
|
450
454
|
renderResult(
|
|
@@ -457,7 +461,7 @@ export const findToolRenderer = {
|
|
|
457
461
|
|
|
458
462
|
if (result.isError || details?.error) {
|
|
459
463
|
const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
|
|
460
|
-
return new Text(formatErrorMessage(errorText, uiTheme),
|
|
464
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
|
|
461
465
|
}
|
|
462
466
|
|
|
463
467
|
const hasDetailedData = details?.fileCount !== undefined;
|
|
@@ -470,14 +474,15 @@ export const findToolRenderer = {
|
|
|
470
474
|
textContent.includes("No files found") ||
|
|
471
475
|
textContent.trim() === ""
|
|
472
476
|
) {
|
|
473
|
-
return new Text(formatEmptyMessage("No files found", uiTheme),
|
|
477
|
+
return new Text(formatEmptyMessage("No files found", uiTheme), 1, 0);
|
|
474
478
|
}
|
|
475
479
|
|
|
476
480
|
const lines = textContent.split("\n").filter(l => l.trim());
|
|
477
481
|
const header = renderStatusLine(
|
|
478
482
|
{
|
|
479
|
-
|
|
483
|
+
iconOverride: findStatusIcon(uiTheme),
|
|
480
484
|
title: "Find",
|
|
485
|
+
titleColor: "toolTitle",
|
|
481
486
|
description: formatFindRenderPaths(args?.paths),
|
|
482
487
|
meta: [formatCount("file", lines.length)],
|
|
483
488
|
},
|
|
@@ -498,6 +503,7 @@ export const findToolRenderer = {
|
|
|
498
503
|
);
|
|
499
504
|
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
500
505
|
},
|
|
506
|
+
{ paddingX: 1 },
|
|
501
507
|
);
|
|
502
508
|
}
|
|
503
509
|
|
|
@@ -513,20 +519,27 @@ export const findToolRenderer = {
|
|
|
513
519
|
|
|
514
520
|
if (fileCount === 0) {
|
|
515
521
|
const header = renderStatusLine(
|
|
516
|
-
{
|
|
522
|
+
{
|
|
523
|
+
icon: "warning",
|
|
524
|
+
title: "Find",
|
|
525
|
+
titleColor: "toolTitle",
|
|
526
|
+
description: formatFindRenderPaths(args?.paths),
|
|
527
|
+
meta: ["0 files"],
|
|
528
|
+
},
|
|
517
529
|
uiTheme,
|
|
518
530
|
);
|
|
519
531
|
const lines = [header, formatEmptyMessage("No files found", uiTheme)];
|
|
520
532
|
if (missingNote) lines.push(missingNote);
|
|
521
|
-
return new Text(lines.join("\n"),
|
|
533
|
+
return new Text(lines.join("\n"), 1, 0);
|
|
522
534
|
}
|
|
523
535
|
const meta: string[] = [formatCount("file", fileCount)];
|
|
524
536
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
525
537
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
526
538
|
const header = renderStatusLine(
|
|
527
539
|
{
|
|
528
|
-
|
|
540
|
+
...(truncated ? { icon: "warning" as const } : { iconOverride: findStatusIcon(uiTheme) }),
|
|
529
541
|
title: "Find",
|
|
542
|
+
titleColor: "toolTitle",
|
|
530
543
|
description: formatFindRenderPaths(args?.paths),
|
|
531
544
|
meta,
|
|
532
545
|
},
|
|
@@ -565,6 +578,7 @@ export const findToolRenderer = {
|
|
|
565
578
|
);
|
|
566
579
|
return [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
567
580
|
},
|
|
581
|
+
{ paddingX: 1 },
|
|
568
582
|
);
|
|
569
583
|
},
|
|
570
584
|
mergeCallAndResult: true,
|