@gajae-code/coding-agent 0.11.2 → 0.11.4
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 +34 -0
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +85 -10
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +55 -10
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +51 -212
- package/src/main.ts +99 -22
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/index.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/sdk/host/host.ts +13 -1
- package/src/sdk/session.ts +4 -1
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/output-meta.ts +6 -2
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +189 -64
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/subagent.ts +43 -2
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
package/src/tools/path-utils.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
-
import { isEnoent } from "@gajae-code/utils";
|
|
5
|
+
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
6
6
|
import { InternalUrlRouter } from "../internal-urls";
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
@@ -10,13 +10,9 @@ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
|
10
10
|
const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*|raw|conflicts)$/i;
|
|
11
11
|
const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*$/i;
|
|
12
12
|
const FILE_RAW_ONLY_RE = /^raw$/i;
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
const INTERNAL_URL_SELECTOR_PART_RE =
|
|
17
|
-
/^(?:raw|conflicts|L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*|-\d+(?:[-+]\d+)?)$/i;
|
|
18
|
-
// Schemes whose host grammar is identifier-shaped, so any trailing
|
|
19
|
-
// `:<selector-chunk>` is unambiguously a read-tool selector.
|
|
13
|
+
// Schemes whose authority grammar is identifier-shaped and may therefore carry
|
|
14
|
+
// a read selector immediately after the authority. Colons in a path, query, or
|
|
15
|
+
// fragment remain part of the URL and are never considered selectors.
|
|
20
16
|
const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
21
17
|
agent: true,
|
|
22
18
|
artifact: true,
|
|
@@ -148,39 +144,50 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
148
144
|
/**
|
|
149
145
|
* Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
|
|
150
146
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* This function iteratively peels selector-shaped chunks (well-formed plus
|
|
159
|
-
* common malformed shapes like `:-N`) so the rest of the read tool can pass a
|
|
160
|
-
* clean URL to the protocol handler and surface selector errors via parseSel
|
|
161
|
-
* instead of as misleading "host invalid" errors from the handler.
|
|
162
|
-
*
|
|
163
|
-
* Falls back to the input unchanged when nothing matches.
|
|
147
|
+
* Artifact authorities are generated identifiers, so their complete tail is
|
|
148
|
+
* unambiguously a selector and malformed selectors can be rejected before
|
|
149
|
+
* resolving the artifact. Other resources may legitimately use colons in
|
|
150
|
+
* their identities, so they only lose strict, recognized selector suffixes.
|
|
151
|
+
* Skill authorities additionally use the active registry to distinguish a
|
|
152
|
+
* skill name from its selector without making path-utils depend on skills.
|
|
164
153
|
*/
|
|
165
|
-
export function splitInternalUrlSel(
|
|
154
|
+
export function splitInternalUrlSel(
|
|
155
|
+
rawPath: string,
|
|
156
|
+
options: { activeSkillNames?: readonly string[] } = {},
|
|
157
|
+
): { path: string; sel?: string } {
|
|
166
158
|
const schemeMatch = rawPath.match(INTERNAL_URL_SCHEME_RE);
|
|
167
159
|
if (!schemeMatch) return { path: rawPath };
|
|
168
|
-
|
|
160
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
161
|
+
if (!INTERNAL_SCHEMES_WITH_SELECTORS[scheme]) return { path: rawPath };
|
|
169
162
|
|
|
170
163
|
const schemeEnd = schemeMatch[0].length;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
164
|
+
const authorityTerminator = rawPath.slice(schemeEnd).search(/[/?#]/);
|
|
165
|
+
if (authorityTerminator !== -1) return { path: rawPath };
|
|
166
|
+
const authority = rawPath.slice(schemeEnd);
|
|
167
|
+
const firstColon = authority.indexOf(":");
|
|
168
|
+
if (firstColon === -1) return { path: rawPath };
|
|
169
|
+
|
|
170
|
+
if (scheme === "artifact") {
|
|
171
|
+
return { path: rawPath.slice(0, schemeEnd + firstColon), sel: authority.slice(firstColon + 1) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (scheme === "skill") {
|
|
175
|
+
const activeSkillNames = options.activeSkillNames ?? [];
|
|
176
|
+
if (activeSkillNames.includes(authority)) return { path: rawPath };
|
|
177
|
+
const skillName = activeSkillNames
|
|
178
|
+
.filter(name => authority.startsWith(`${name}:`))
|
|
179
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
180
|
+
if (skillName) {
|
|
181
|
+
return {
|
|
182
|
+
path: `${rawPath.slice(0, schemeEnd)}${skillName}`,
|
|
183
|
+
sel: authority.slice(skillName.length + 1),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
181
186
|
}
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
|
|
188
|
+
const strict = splitPathAndSel(authority);
|
|
189
|
+
if (strict.sel === undefined) return { path: rawPath };
|
|
190
|
+
return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}`, sel: strict.sel };
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
function assertNotInternalUrl(expanded: string, original: string): void {
|
package/src/tools/read.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as z from "zod/v4";
|
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
+
import { getActiveSkills } from "../extensibility/skills";
|
|
14
15
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
15
16
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
17
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
@@ -95,6 +96,8 @@ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx"
|
|
|
95
96
|
|
|
96
97
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
97
98
|
const MAX_SUMMARY_LINES = 20_000;
|
|
99
|
+
const RAW_COLLECTOR_MAX_BYTES = MAX_SUMMARY_BYTES;
|
|
100
|
+
|
|
98
101
|
/**
|
|
99
102
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
100
103
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -193,6 +196,14 @@ function formatSummaryElisionFooter(readPath: string, elidedSpans: number, elide
|
|
|
193
196
|
const linePart = elidedLines > 0 ? `${elidedLines} ${lineWord} across ` : "";
|
|
194
197
|
return `[${linePart}${elidedSpans} elided ${spanWord}; read ${readPath}:raw or a line range like ${readPath}:1-9999 for verbatim content]`;
|
|
195
198
|
}
|
|
199
|
+
|
|
200
|
+
function formatReceiptFooter(readPath: string, shownLines: number, totalLines: number, shownBytes: number): string {
|
|
201
|
+
return `[Showing first ${shownLines} of ${totalLines} lines (~${Math.ceil(shownBytes / 1024)} KiB); re-read ${readPath}:1-${totalLines} or ${readPath}:raw for the full file]`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatSummaryCapFooter(readPath: string, maxBytes: number, totalLines: number): string {
|
|
205
|
+
return `[Summary truncated at ${Math.ceil(maxBytes / 1024)} KiB; re-read ${readPath}:raw or ${readPath}:1-${totalLines} for the full source]`;
|
|
206
|
+
}
|
|
196
207
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
197
208
|
|
|
198
209
|
/**
|
|
@@ -532,6 +543,9 @@ export interface ReadToolDetails {
|
|
|
532
543
|
method?: string;
|
|
533
544
|
notes?: string[];
|
|
534
545
|
meta?: OutputMeta;
|
|
546
|
+
/** Whether this explicit content read may be spilled by output metadata. */
|
|
547
|
+
spillEligible?: boolean;
|
|
548
|
+
|
|
535
549
|
/** Raw text + start line for user-visible TUI rendering, set when content is text-like.
|
|
536
550
|
* Mirrors the same lines the model receives but without hashline/line-number prefixes,
|
|
537
551
|
* so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
|
|
@@ -816,10 +830,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
816
830
|
options: {
|
|
817
831
|
details?: ReadToolDetails;
|
|
818
832
|
sourcePath?: string;
|
|
833
|
+
spillEligible?: boolean;
|
|
834
|
+
receiptPath?: string;
|
|
819
835
|
sourceUrl?: string;
|
|
820
836
|
sourceInternal?: string;
|
|
821
837
|
entityLabel: string;
|
|
822
838
|
ignoreResultLimits?: boolean;
|
|
839
|
+
resultByteCeiling?: number;
|
|
823
840
|
raw?: boolean;
|
|
824
841
|
immutable?: boolean;
|
|
825
842
|
wrapUntrusted?: boolean;
|
|
@@ -871,9 +888,30 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
871
888
|
}
|
|
872
889
|
|
|
873
890
|
const endLine = endLineExpanded;
|
|
874
|
-
|
|
891
|
+
let selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
892
|
+
|
|
893
|
+
if (options.receiptPath) {
|
|
894
|
+
const budgetLines = this.session.settings.get("read.receiptBudgetLines");
|
|
895
|
+
const budgetBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
896
|
+
const receiptLines: string[] = [];
|
|
897
|
+
let bytes = 0;
|
|
898
|
+
for (const line of allLines) {
|
|
899
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (receiptLines.length > 0 ? 1 : 0);
|
|
900
|
+
if (receiptLines.length >= budgetLines || bytes + lineBytes > budgetBytes) break;
|
|
901
|
+
receiptLines.push(line);
|
|
902
|
+
bytes += lineBytes;
|
|
903
|
+
}
|
|
904
|
+
selectedContent = receiptLines.join("\n");
|
|
905
|
+
}
|
|
875
906
|
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
876
|
-
const truncation = ignoreResultLimits
|
|
907
|
+
const truncation = ignoreResultLimits
|
|
908
|
+
? noTruncResult(selectedContent)
|
|
909
|
+
: truncateHead(
|
|
910
|
+
selectedContent,
|
|
911
|
+
options.resultByteCeiling === undefined
|
|
912
|
+
? undefined
|
|
913
|
+
: { maxBytes: options.resultByteCeiling, maxLines: Number.MAX_SAFE_INTEGER },
|
|
914
|
+
);
|
|
877
915
|
|
|
878
916
|
const shouldAddHashLines = displayMode.hashLines;
|
|
879
917
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
@@ -927,7 +965,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
927
965
|
} else {
|
|
928
966
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
929
967
|
}
|
|
968
|
+
if (options.receiptPath && (truncation.truncated || countTextLines(selectedContent) < totalLines)) {
|
|
969
|
+
outputText += `\n\n${formatReceiptFooter(options.receiptPath, countTextLines(selectedContent), totalLines, Buffer.byteLength(selectedContent, "utf-8"))}`;
|
|
970
|
+
}
|
|
930
971
|
|
|
972
|
+
details.spillEligible = options.spillEligible === true && selectedContent.length > 0;
|
|
931
973
|
resultBuilder.text(options.wrapUntrusted ? wrapUntrustedContent(outputText) : outputText);
|
|
932
974
|
if (truncationInfo) {
|
|
933
975
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -987,6 +1029,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
987
1029
|
}
|
|
988
1030
|
const finalText =
|
|
989
1031
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1032
|
+
details.spillEligible = parts.length > 0;
|
|
1033
|
+
|
|
990
1034
|
resultBuilder.text(finalText);
|
|
991
1035
|
return resultBuilder.done();
|
|
992
1036
|
}
|
|
@@ -1006,6 +1050,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1006
1050
|
signal: AbortSignal | undefined,
|
|
1007
1051
|
): Promise<{
|
|
1008
1052
|
outputText: string;
|
|
1053
|
+
contentLineCount: number;
|
|
1009
1054
|
columnTruncated: number;
|
|
1010
1055
|
bridgeResult?: AgentToolResult<ReadToolDetails>;
|
|
1011
1056
|
}> {
|
|
@@ -1027,7 +1072,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1027
1072
|
const firstText = bridgeResult.content.find((c): c is TextContent => c.type === "text");
|
|
1028
1073
|
if (firstText) firstText.text = `${notice}\n${firstText.text}`;
|
|
1029
1074
|
}
|
|
1030
|
-
return { outputText: "", columnTruncated: 0, bridgeResult };
|
|
1075
|
+
return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult };
|
|
1031
1076
|
} catch (error) {
|
|
1032
1077
|
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1033
1078
|
}
|
|
@@ -1040,6 +1085,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1040
1085
|
const blocks: string[] = [];
|
|
1041
1086
|
const notices: string[] = [];
|
|
1042
1087
|
let columnTruncated = 0;
|
|
1088
|
+
let contentLineCount = 0;
|
|
1043
1089
|
|
|
1044
1090
|
for (const range of ranges) {
|
|
1045
1091
|
const rangeStart = range.startLine - 1; // 0-indexed
|
|
@@ -1074,6 +1120,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1074
1120
|
}
|
|
1075
1121
|
}
|
|
1076
1122
|
|
|
1123
|
+
contentLineCount += collectedLines.length;
|
|
1077
1124
|
if (collectedLines.length > 0) {
|
|
1078
1125
|
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine, collectedLines);
|
|
1079
1126
|
}
|
|
@@ -1086,7 +1133,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1086
1133
|
if (notices.length > 0) {
|
|
1087
1134
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
1088
1135
|
}
|
|
1089
|
-
return { outputText, columnTruncated };
|
|
1136
|
+
return { outputText, contentLineCount, columnTruncated };
|
|
1090
1137
|
}
|
|
1091
1138
|
|
|
1092
1139
|
async #readArchiveDirectory(
|
|
@@ -1369,6 +1416,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1369
1416
|
displayText: string;
|
|
1370
1417
|
elidedSpans: number;
|
|
1371
1418
|
elidedLines: number;
|
|
1419
|
+
capped: boolean;
|
|
1372
1420
|
} {
|
|
1373
1421
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
1374
1422
|
const shouldAddHashLines = displayMode.hashLines;
|
|
@@ -1428,17 +1476,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1428
1476
|
|
|
1429
1477
|
const modelParts: string[] = [];
|
|
1430
1478
|
const displayParts: string[] = [];
|
|
1479
|
+
const capBytes = this.session.settings.get("read.summaryMaxBytes") * 1024;
|
|
1480
|
+
let usedBytes = 0;
|
|
1431
1481
|
let elidedSpans = 0;
|
|
1432
1482
|
let elidedLines = 0;
|
|
1483
|
+
let capDropped = false;
|
|
1433
1484
|
for (const unit of units) {
|
|
1485
|
+
let model: string;
|
|
1486
|
+
let display: string;
|
|
1487
|
+
let implicitElidedLines = 0;
|
|
1434
1488
|
if (unit.kind === "elided") {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
elidedLines += unit.endLine - unit.startLine + 1;
|
|
1439
|
-
continue;
|
|
1440
|
-
}
|
|
1441
|
-
if (unit.kind === "merged") {
|
|
1489
|
+
model = display = "...";
|
|
1490
|
+
implicitElidedLines = unit.endLine - unit.startLine + 1;
|
|
1491
|
+
} else if (unit.kind === "merged") {
|
|
1442
1492
|
const formatted = formatMergedBraceLine(
|
|
1443
1493
|
unit.startLine,
|
|
1444
1494
|
unit.endLine,
|
|
@@ -1447,18 +1497,39 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1447
1497
|
shouldAddHashLines,
|
|
1448
1498
|
shouldAddLineNumbers,
|
|
1449
1499
|
);
|
|
1450
|
-
|
|
1451
|
-
|
|
1500
|
+
model = formatted.model;
|
|
1501
|
+
display = formatted.display;
|
|
1502
|
+
implicitElidedLines = Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1503
|
+
} else {
|
|
1504
|
+
model = formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers);
|
|
1505
|
+
display = unit.text;
|
|
1506
|
+
}
|
|
1507
|
+
const separatorBytes = modelParts.length > 0 ? 1 : 0;
|
|
1508
|
+
if (usedBytes + separatorBytes + Buffer.byteLength(model, "utf-8") > capBytes) {
|
|
1509
|
+
capDropped = true;
|
|
1452
1510
|
elidedSpans++;
|
|
1453
|
-
|
|
1454
|
-
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1511
|
+
elidedLines += unit.kind === "line" ? 1 : unit.endLine - unit.startLine + 1;
|
|
1455
1512
|
continue;
|
|
1456
1513
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1514
|
+
usedBytes += separatorBytes + Buffer.byteLength(model, "utf-8");
|
|
1515
|
+
modelParts.push(model);
|
|
1516
|
+
displayParts.push(display);
|
|
1517
|
+
if (unit.kind === "elided" || unit.kind === "merged") {
|
|
1518
|
+
elidedSpans++;
|
|
1519
|
+
elidedLines += implicitElidedLines;
|
|
1520
|
+
}
|
|
1459
1521
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1522
|
+
if (capDropped) {
|
|
1523
|
+
modelParts.push("…");
|
|
1524
|
+
displayParts.push("…");
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
text: modelParts.join("\n"),
|
|
1528
|
+
displayText: displayParts.join("\n"),
|
|
1529
|
+
elidedSpans,
|
|
1530
|
+
elidedLines,
|
|
1531
|
+
capped: capDropped,
|
|
1532
|
+
};
|
|
1462
1533
|
}
|
|
1463
1534
|
|
|
1464
1535
|
async execute(
|
|
@@ -1515,8 +1586,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1515
1586
|
// off the URL and surfaced via parseSel rather than confusing handlers.
|
|
1516
1587
|
const internalRouter = InternalUrlRouter.instance();
|
|
1517
1588
|
if (internalRouter.canHandle(readPath)) {
|
|
1518
|
-
const internalTarget = splitInternalUrlSel(readPath
|
|
1589
|
+
const internalTarget = splitInternalUrlSel(readPath, {
|
|
1590
|
+
activeSkillNames: getActiveSkills().map(skill => skill.name),
|
|
1591
|
+
});
|
|
1519
1592
|
const parsed = parseSel(internalTarget.sel);
|
|
1593
|
+
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
1594
|
+
throw new ToolError(`Invalid internal URL selector "${internalTarget.sel}".`);
|
|
1595
|
+
}
|
|
1520
1596
|
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
1521
1597
|
}
|
|
1522
1598
|
|
|
@@ -1649,25 +1725,38 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1649
1725
|
details: { resolvedPath: absolutePath },
|
|
1650
1726
|
sourcePath: absolutePath,
|
|
1651
1727
|
entityLabel: "notebook",
|
|
1728
|
+
spillEligible: parsed.kind !== "none",
|
|
1729
|
+
resultByteCeiling:
|
|
1730
|
+
parsed.kind !== "none"
|
|
1731
|
+
? Math.max(
|
|
1732
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1733
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1734
|
+
)
|
|
1735
|
+
: undefined,
|
|
1736
|
+
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
1652
1737
|
});
|
|
1653
1738
|
} else if (shouldConvertWithMarkit) {
|
|
1654
|
-
// Convert document via markit.
|
|
1655
1739
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1656
1740
|
if (result.ok) {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1741
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1742
|
+
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1743
|
+
details: { resolvedPath: absolutePath },
|
|
1744
|
+
sourcePath: absolutePath,
|
|
1745
|
+
entityLabel: "converted document",
|
|
1746
|
+
raw: isRawSelector(parsed),
|
|
1747
|
+
spillEligible: parsed.kind !== "none",
|
|
1748
|
+
resultByteCeiling:
|
|
1749
|
+
parsed.kind !== "none"
|
|
1750
|
+
? Math.max(
|
|
1751
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1752
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1753
|
+
)
|
|
1754
|
+
: undefined,
|
|
1755
|
+
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
1756
|
+
});
|
|
1670
1757
|
}
|
|
1758
|
+
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
1759
|
+
details = { spillEligible: false };
|
|
1671
1760
|
} else {
|
|
1672
1761
|
if (
|
|
1673
1762
|
parsed.kind === "none" &&
|
|
@@ -1682,8 +1771,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1682
1771
|
renderedSummary.elidedSpans,
|
|
1683
1772
|
renderedSummary.elidedLines,
|
|
1684
1773
|
);
|
|
1685
|
-
const
|
|
1774
|
+
const summaryTotalLines = Math.max(...summary.segments.map(segment => segment.endLine));
|
|
1775
|
+
const modelText = [
|
|
1776
|
+
renderedSummary.text,
|
|
1777
|
+
footer,
|
|
1778
|
+
renderedSummary.capped
|
|
1779
|
+
? formatSummaryCapFooter(
|
|
1780
|
+
localReadPath,
|
|
1781
|
+
this.session.settings.get("read.summaryMaxBytes") * 1024,
|
|
1782
|
+
summaryTotalLines,
|
|
1783
|
+
)
|
|
1784
|
+
: "",
|
|
1785
|
+
]
|
|
1786
|
+
.filter(Boolean)
|
|
1787
|
+
.join("\n\n");
|
|
1686
1788
|
details = {
|
|
1789
|
+
spillEligible: false,
|
|
1687
1790
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1688
1791
|
summary: {
|
|
1689
1792
|
lines: countTextLines(renderedSummary.text),
|
|
@@ -1710,7 +1813,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1710
1813
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1711
1814
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
1712
1815
|
sourcePath = absolutePath;
|
|
1713
|
-
details = {};
|
|
1816
|
+
details = { spillEligible: multiResult.contentLineCount > 0 };
|
|
1714
1817
|
if (multiResult.columnTruncated > 0) {
|
|
1715
1818
|
columnTruncated = multiResult.columnTruncated;
|
|
1716
1819
|
}
|
|
@@ -1750,13 +1853,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1750
1853
|
const startLine = requestedStart - leadingContext;
|
|
1751
1854
|
const startLineDisplay = startLine + 1;
|
|
1752
1855
|
|
|
1856
|
+
const isBareReceipt = parsed.kind === "none" && !content;
|
|
1857
|
+
const rawSelector = isRawSelector(parsed);
|
|
1753
1858
|
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1754
|
-
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1755
|
-
const
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1859
|
+
const effectiveLimit = rawSelector ? Number.MAX_SAFE_INTEGER : (limit ?? DEFAULT_LIMIT);
|
|
1860
|
+
const receiptBudgetBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
1861
|
+
const maxLinesToCollect = rawSelector
|
|
1862
|
+
? Number.MAX_SAFE_INTEGER
|
|
1863
|
+
: isBareReceipt
|
|
1864
|
+
? this.session.settings.get("read.receiptBudgetLines")
|
|
1865
|
+
: Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
1866
|
+
const selectedLineLimit = rawSelector ? null : effectiveLimit + leadingContext + trailingContext;
|
|
1867
|
+
const maxBytesForRead = rawSelector
|
|
1868
|
+
? Math.max(
|
|
1869
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1870
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1871
|
+
)
|
|
1872
|
+
: isBareReceipt
|
|
1873
|
+
? receiptBudgetBytes
|
|
1874
|
+
: Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
1760
1875
|
|
|
1761
1876
|
const streamResult = await streamLinesFromFile(
|
|
1762
1877
|
absolutePath,
|
|
@@ -1793,7 +1908,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1793
1908
|
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
1794
1909
|
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
1795
1910
|
// view — column truncation surfaces separately via `.limits()`.
|
|
1796
|
-
|
|
1911
|
+
|
|
1797
1912
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1798
1913
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1799
1914
|
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
@@ -1855,7 +1970,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1855
1970
|
firstLineBytes,
|
|
1856
1971
|
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1857
1972
|
}
|
|
1858
|
-
details = { truncation };
|
|
1973
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1974
|
+
|
|
1859
1975
|
sourcePath = absolutePath;
|
|
1860
1976
|
truncationInfo = {
|
|
1861
1977
|
result: truncation,
|
|
@@ -1863,7 +1979,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1863
1979
|
};
|
|
1864
1980
|
} else if (truncation.truncated) {
|
|
1865
1981
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1866
|
-
details = { truncation };
|
|
1982
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1983
|
+
|
|
1867
1984
|
sourcePath = absolutePath;
|
|
1868
1985
|
truncationInfo = {
|
|
1869
1986
|
result: truncation,
|
|
@@ -1875,14 +1992,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1875
1992
|
|
|
1876
1993
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1877
1994
|
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1878
|
-
details = {};
|
|
1995
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1996
|
+
|
|
1879
1997
|
sourcePath = absolutePath;
|
|
1880
1998
|
} else {
|
|
1881
1999
|
// No truncation, no user limit exceeded
|
|
1882
2000
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1883
|
-
details = {};
|
|
2001
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
2002
|
+
|
|
1884
2003
|
sourcePath = absolutePath;
|
|
1885
2004
|
}
|
|
2005
|
+
if (isBareReceipt && (truncation.truncated || collectedLines.length < totalFileLines)) {
|
|
2006
|
+
outputText += `\n\n${formatReceiptFooter(localReadPath, collectedLines.length, totalFileLines, collectedBytes)}`;
|
|
2007
|
+
}
|
|
2008
|
+
if (rawSelector && truncation.truncated) {
|
|
2009
|
+
outputText += `\n\n[Raw read capped at ${formatBytes(maxBytesForRead)} of ${formatBytes(fileSize)}; re-read a line range to narrow the view]`;
|
|
2010
|
+
}
|
|
1886
2011
|
|
|
1887
2012
|
if (capturedDisplayContent) {
|
|
1888
2013
|
details.displayContent = capturedDisplayContent;
|
|
@@ -2109,23 +2234,23 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2109
2234
|
}
|
|
2110
2235
|
throwIfAborted(signal);
|
|
2111
2236
|
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
return
|
|
2237
|
+
const receiptLines = this.session.settings.get("read.receiptBudgetLines");
|
|
2238
|
+
const receiptBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
2239
|
+
const sourceLines = tree.totalLines <= 1 ? ["(empty directory)"] : tree.rendered.split("\n");
|
|
2240
|
+
const bodyLines: string[] = [];
|
|
2241
|
+
let bodyBytes = 0;
|
|
2242
|
+
for (const line of sourceLines) {
|
|
2243
|
+
const bytes = Buffer.byteLength(line, "utf-8") + (bodyLines.length > 0 ? 1 : 0);
|
|
2244
|
+
if (bodyLines.length >= receiptLines || bodyBytes + bytes > receiptBytes) break;
|
|
2245
|
+
bodyLines.push(line);
|
|
2246
|
+
bodyBytes += bytes;
|
|
2247
|
+
}
|
|
2248
|
+
let output = bodyLines.join("\n");
|
|
2249
|
+
if (bodyLines.length < sourceLines.length || tree.truncated) {
|
|
2250
|
+
output += `\n\n[Listing truncated at ${bodyLines.length} lines / ${Math.ceil(bodyBytes / 1024)} KiB; read a deeper subpath (e.g. ${absolutePath}/<subdir>) to narrow the view]`;
|
|
2251
|
+
}
|
|
2252
|
+
const details: ReadToolDetails = { isDirectory: true, spillEligible: false, resolvedPath: tree.rootPath };
|
|
2253
|
+
return toolResult(details).text(output).sourcePath(tree.rootPath).done();
|
|
2129
2254
|
}
|
|
2130
2255
|
}
|
|
2131
2256
|
|
|
@@ -15,10 +15,10 @@ import { pluralize } from "@gajae-code/utils";
|
|
|
15
15
|
import { settings } from "../config/settings";
|
|
16
16
|
import type { Theme } from "../modes/theme/theme";
|
|
17
17
|
import { Hasher } from "../tui/utils";
|
|
18
|
-
import { formatDimensionNote, type ResizedImage } from "../utils/image-resize";
|
|
19
18
|
|
|
20
19
|
export { Ellipsis } from "@gajae-code/natives";
|
|
21
20
|
export { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "@gajae-code/tui";
|
|
21
|
+
export { formatScreenshot } from "./browser/screenshot-format";
|
|
22
22
|
|
|
23
23
|
// =============================================================================
|
|
24
24
|
// Standardized Display Constants
|
|
@@ -591,32 +591,6 @@ export function formatToolWorkingDirectory(workdir: string | undefined, projectD
|
|
|
591
591
|
return replaceTabs(displayWorkdir);
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
export function formatScreenshot(opts: {
|
|
595
|
-
saveFullRes: boolean;
|
|
596
|
-
savedMimeType: string;
|
|
597
|
-
savedByteLength: number;
|
|
598
|
-
dest: string;
|
|
599
|
-
resized: ResizedImage;
|
|
600
|
-
}): string[] {
|
|
601
|
-
const lines = ["Screenshot captured"];
|
|
602
|
-
if (opts.saveFullRes) {
|
|
603
|
-
lines.push(
|
|
604
|
-
`Saved: ${opts.savedMimeType} (${(opts.savedByteLength / 1024).toFixed(2)} KB) to ${shortenPath(opts.dest)}`,
|
|
605
|
-
);
|
|
606
|
-
lines.push(
|
|
607
|
-
`Model: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB, ${opts.resized.width}x${opts.resized.height})`,
|
|
608
|
-
);
|
|
609
|
-
} else {
|
|
610
|
-
lines.push(`Format: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB)`);
|
|
611
|
-
lines.push(`Dimensions: ${opts.resized.width}x${opts.resized.height}`);
|
|
612
|
-
}
|
|
613
|
-
const dimensionNote = formatDimensionNote(opts.resized);
|
|
614
|
-
if (dimensionNote) {
|
|
615
|
-
lines.push(dimensionNote);
|
|
616
|
-
}
|
|
617
|
-
return lines;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
594
|
export function wrapBrackets(text: string, theme: Theme): string {
|
|
621
595
|
return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
|
|
622
596
|
}
|