@makefinks/daemon 0.8.0 → 0.9.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/package.json +11 -3
- package/src/ai/daemon-ai.ts +3 -3
- package/src/ai/memory/memory-manager.ts +5 -1
- package/src/ai/system-prompt.ts +47 -41
- package/src/ai/tools/fetch-urls.ts +166 -125
- package/src/components/SettingsMenu.tsx +36 -27
- package/src/components/UrlMenu.tsx +2 -7
- package/src/components/tool-layouts/layouts/subagent.tsx +16 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +145 -81
- package/src/hooks/keyboard-handlers.ts +22 -31
- package/src/types/index.ts +0 -1
- package/src/utils/derive-url-menu-items.ts +198 -37
- package/src/utils/preferences.ts +3 -0
- package/src/utils/tool-output-preview.ts +116 -27
|
@@ -10,6 +10,81 @@ function normalizeUrlKey(rawUrl: string): string {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function unescapeXmlAttribute(value: string): string {
|
|
14
|
+
return value
|
|
15
|
+
.replace(/"/g, '"')
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/&/g, "&");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseFetchUrlsXml(result: string): {
|
|
22
|
+
items: Array<{
|
|
23
|
+
url: string;
|
|
24
|
+
lineOffset?: number;
|
|
25
|
+
lineLimit?: number;
|
|
26
|
+
totalLines?: number;
|
|
27
|
+
remainingLines?: number | null;
|
|
28
|
+
error?: string;
|
|
29
|
+
success: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
error?: string;
|
|
32
|
+
} {
|
|
33
|
+
const items: Array<{
|
|
34
|
+
url: string;
|
|
35
|
+
lineOffset?: number;
|
|
36
|
+
lineLimit?: number;
|
|
37
|
+
totalLines?: number;
|
|
38
|
+
remainingLines?: number | null;
|
|
39
|
+
error?: string;
|
|
40
|
+
success: boolean;
|
|
41
|
+
}> = [];
|
|
42
|
+
|
|
43
|
+
let error: string | undefined;
|
|
44
|
+
const errorMatch = result.match(/<fetchUrls[^>]*\serror="([^"]+)"[^>]*\/?>/);
|
|
45
|
+
if (errorMatch?.[1]) {
|
|
46
|
+
error = unescapeXmlAttribute(errorMatch[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const regex = /<url\s+([^>]*?)(\/?)>/g;
|
|
50
|
+
let match = regex.exec(result);
|
|
51
|
+
while (match) {
|
|
52
|
+
const attrText = match[1] ?? "";
|
|
53
|
+
const attrs: Record<string, string> = {};
|
|
54
|
+
for (const attr of attrText.matchAll(/(\w+)="([^"]*)"/g)) {
|
|
55
|
+
const key = attr[1];
|
|
56
|
+
if (!key) continue;
|
|
57
|
+
const value = attr[2] ?? "";
|
|
58
|
+
attrs[key] = unescapeXmlAttribute(value);
|
|
59
|
+
}
|
|
60
|
+
const url = attrs.href;
|
|
61
|
+
if (!url) continue;
|
|
62
|
+
const lineOffset = attrs.lineOffset !== undefined ? Number(attrs.lineOffset) : undefined;
|
|
63
|
+
const lineLimit = attrs.lineLimit !== undefined ? Number(attrs.lineLimit) : undefined;
|
|
64
|
+
const totalLines = attrs.totalLines !== undefined ? Number(attrs.totalLines) : undefined;
|
|
65
|
+
let remainingLines: number | null | undefined = undefined;
|
|
66
|
+
if (attrs.remainingLines === "unknown") remainingLines = null;
|
|
67
|
+
else if (attrs.remainingLines !== undefined) remainingLines = Number(attrs.remainingLines);
|
|
68
|
+
const error = attrs.error;
|
|
69
|
+
const success = !error;
|
|
70
|
+
items.push({
|
|
71
|
+
url,
|
|
72
|
+
lineOffset: Number.isFinite(lineOffset) ? lineOffset : undefined,
|
|
73
|
+
lineLimit: Number.isFinite(lineLimit) ? lineLimit : undefined,
|
|
74
|
+
totalLines: Number.isFinite(totalLines) ? totalLines : undefined,
|
|
75
|
+
remainingLines:
|
|
76
|
+
remainingLines === null || (typeof remainingLines === "number" && Number.isFinite(remainingLines))
|
|
77
|
+
? remainingLines
|
|
78
|
+
: undefined,
|
|
79
|
+
error,
|
|
80
|
+
success,
|
|
81
|
+
});
|
|
82
|
+
match = regex.exec(result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { items, error };
|
|
86
|
+
}
|
|
87
|
+
|
|
13
88
|
function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
|
|
14
89
|
if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
|
|
15
90
|
if (intervals.length === 0) return undefined;
|
|
@@ -43,7 +118,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
43
118
|
|
|
44
119
|
const intervalsByUrl = new Map<string, Array<[number, number]>>();
|
|
45
120
|
const totalLinesByUrl = new Map<string, number>();
|
|
46
|
-
const highlightsCountByUrl = new Map<string, number>();
|
|
47
121
|
const lastSeenIndexByUrl = new Map<string, number>();
|
|
48
122
|
const statusByUrl = new Map<string, "ok" | "error">();
|
|
49
123
|
const errorByUrl = new Map<string, string>();
|
|
@@ -57,58 +131,147 @@ export function deriveUrlMenuItems(params: {
|
|
|
57
131
|
if (block.type !== "tool") continue;
|
|
58
132
|
if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
|
|
59
133
|
|
|
60
|
-
const input = block.call.input as { url?: string } | undefined;
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
134
|
+
const input = block.call.input as { url?: string; requests?: Array<{ url?: string }> } | undefined;
|
|
135
|
+
const urls: string[] = [];
|
|
136
|
+
if (input?.url && typeof input.url === "string") {
|
|
137
|
+
urls.push(input.url);
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(input?.requests)) {
|
|
140
|
+
for (const request of input.requests) {
|
|
141
|
+
if (request?.url && typeof request.url === "string") {
|
|
142
|
+
urls.push(request.url);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (urls.length === 0) continue;
|
|
63
147
|
|
|
64
|
-
|
|
148
|
+
for (const url of urls) {
|
|
149
|
+
lastSeenIndexByUrl.set(url, blockIndex);
|
|
150
|
+
}
|
|
65
151
|
|
|
66
152
|
const result = block.result as
|
|
67
153
|
| {
|
|
154
|
+
success?: boolean;
|
|
155
|
+
error?: string;
|
|
156
|
+
results?: Array<{
|
|
157
|
+
success?: boolean;
|
|
158
|
+
url?: string;
|
|
159
|
+
lineOffset?: number;
|
|
160
|
+
lineLimit?: number;
|
|
161
|
+
totalLines?: number;
|
|
162
|
+
error?: string;
|
|
163
|
+
}>;
|
|
68
164
|
lineOffset?: number;
|
|
69
165
|
lineLimit?: number;
|
|
70
166
|
totalLines?: number;
|
|
71
|
-
highlights?: unknown[];
|
|
72
|
-
success?: boolean;
|
|
73
|
-
error?: string;
|
|
74
167
|
}
|
|
75
168
|
| undefined;
|
|
76
169
|
|
|
77
|
-
if (!result
|
|
170
|
+
if (!result) continue;
|
|
78
171
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
statusByUrl.set(url, "ok");
|
|
84
|
-
}
|
|
172
|
+
if (typeof result === "string") {
|
|
173
|
+
const parsed = parseFetchUrlsXml(result);
|
|
174
|
+
for (const item of parsed.items) {
|
|
175
|
+
const itemUrl = item.url;
|
|
85
176
|
|
|
86
|
-
|
|
87
|
-
|
|
177
|
+
if (!item.success && item.error && item.error.trim().length > 0) {
|
|
178
|
+
statusByUrl.set(itemUrl, "error");
|
|
179
|
+
errorByUrl.set(itemUrl, item.error.trim());
|
|
180
|
+
} else if (item.success) {
|
|
181
|
+
statusByUrl.set(itemUrl, "ok");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (typeof item.totalLines === "number" && item.totalLines > 0) {
|
|
185
|
+
const prev = totalLinesByUrl.get(itemUrl) ?? 0;
|
|
186
|
+
totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
typeof item.lineOffset === "number" &&
|
|
191
|
+
typeof item.lineLimit === "number" &&
|
|
192
|
+
item.lineLimit > 0 &&
|
|
193
|
+
item.lineOffset >= 0
|
|
194
|
+
) {
|
|
195
|
+
const start = item.lineOffset;
|
|
196
|
+
const end = item.lineOffset + item.lineLimit;
|
|
197
|
+
const list = intervalsByUrl.get(itemUrl) ?? [];
|
|
198
|
+
list.push([start, end]);
|
|
199
|
+
intervalsByUrl.set(itemUrl, list);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (parsed.items.length === 0 && parsed.error) {
|
|
203
|
+
for (const url of urls) {
|
|
204
|
+
statusByUrl.set(url, "error");
|
|
205
|
+
errorByUrl.set(url, parsed.error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
88
209
|
}
|
|
89
210
|
|
|
90
|
-
if (
|
|
211
|
+
if (typeof result !== "object") continue;
|
|
212
|
+
|
|
213
|
+
if (Array.isArray(result.results)) {
|
|
214
|
+
for (const item of result.results) {
|
|
215
|
+
if (!item || typeof item !== "object") continue;
|
|
216
|
+
const itemUrl = typeof item.url === "string" ? item.url : null;
|
|
217
|
+
if (!itemUrl) continue;
|
|
218
|
+
|
|
219
|
+
if (item.success === false && typeof item.error === "string" && item.error.trim().length > 0) {
|
|
220
|
+
statusByUrl.set(itemUrl, "error");
|
|
221
|
+
errorByUrl.set(itemUrl, item.error.trim());
|
|
222
|
+
} else if (item.success === true) {
|
|
223
|
+
statusByUrl.set(itemUrl, "ok");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof item.totalLines === "number" && Number.isFinite(item.totalLines) && item.totalLines > 0) {
|
|
227
|
+
const prev = totalLinesByUrl.get(itemUrl) ?? 0;
|
|
228
|
+
totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
typeof item.lineOffset === "number" &&
|
|
233
|
+
typeof item.lineLimit === "number" &&
|
|
234
|
+
Number.isFinite(item.lineOffset) &&
|
|
235
|
+
Number.isFinite(item.lineLimit) &&
|
|
236
|
+
item.lineLimit > 0 &&
|
|
237
|
+
item.lineOffset >= 0
|
|
238
|
+
) {
|
|
239
|
+
const start = item.lineOffset;
|
|
240
|
+
const end = item.lineOffset + item.lineLimit;
|
|
241
|
+
const list = intervalsByUrl.get(itemUrl) ?? [];
|
|
242
|
+
list.push([start, end]);
|
|
243
|
+
intervalsByUrl.set(itemUrl, list);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (
|
|
247
|
+
result.success === false &&
|
|
248
|
+
typeof result.error === "string" &&
|
|
249
|
+
result.error.trim().length > 0
|
|
250
|
+
) {
|
|
251
|
+
for (const url of urls) {
|
|
252
|
+
statusByUrl.set(url, "error");
|
|
253
|
+
errorByUrl.set(url, result.error.trim());
|
|
254
|
+
}
|
|
255
|
+
} else if (result.success === true) {
|
|
256
|
+
for (const url of urls) {
|
|
257
|
+
statusByUrl.set(url, "ok");
|
|
258
|
+
}
|
|
259
|
+
} else if (
|
|
91
260
|
typeof result.totalLines === "number" &&
|
|
92
261
|
Number.isFinite(result.totalLines) &&
|
|
93
|
-
result.totalLines > 0
|
|
94
|
-
) {
|
|
95
|
-
const prev = totalLinesByUrl.get(url) ?? 0;
|
|
96
|
-
totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (
|
|
262
|
+
result.totalLines > 0 &&
|
|
100
263
|
typeof result.lineOffset === "number" &&
|
|
101
|
-
typeof result.lineLimit === "number"
|
|
102
|
-
Number.isFinite(result.lineOffset) &&
|
|
103
|
-
Number.isFinite(result.lineLimit) &&
|
|
104
|
-
result.lineLimit > 0 &&
|
|
105
|
-
result.lineOffset >= 0
|
|
264
|
+
typeof result.lineLimit === "number"
|
|
106
265
|
) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
266
|
+
for (const url of urls) {
|
|
267
|
+
const prev = totalLinesByUrl.get(url) ?? 0;
|
|
268
|
+
totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
|
|
269
|
+
const start = result.lineOffset;
|
|
270
|
+
const end = result.lineOffset + result.lineLimit;
|
|
271
|
+
const list = intervalsByUrl.get(url) ?? [];
|
|
272
|
+
list.push([start, end]);
|
|
273
|
+
intervalsByUrl.set(url, list);
|
|
274
|
+
}
|
|
112
275
|
}
|
|
113
276
|
}
|
|
114
277
|
|
|
@@ -134,7 +297,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
134
297
|
const urls = [...lastSeenIndexByUrl.keys()];
|
|
135
298
|
return urls.map((url) => {
|
|
136
299
|
const groundedCount = lookupGroundedCount(url);
|
|
137
|
-
const highlightsCount = highlightsCountByUrl.get(url);
|
|
138
300
|
const totalLines = totalLinesByUrl.get(url);
|
|
139
301
|
const intervals = intervalsByUrl.get(url) ?? [];
|
|
140
302
|
const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
|
|
@@ -146,7 +308,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
146
308
|
url,
|
|
147
309
|
groundedCount,
|
|
148
310
|
readPercent,
|
|
149
|
-
highlightsCount,
|
|
150
311
|
status,
|
|
151
312
|
error,
|
|
152
313
|
lastSeenIndex,
|
package/src/utils/preferences.ts
CHANGED
|
@@ -12,6 +12,7 @@ const PREFERENCES_VERSION = 1;
|
|
|
12
12
|
const APP_DIR_NAME = "daemon";
|
|
13
13
|
const PREFERENCES_FILE = "preferences.json";
|
|
14
14
|
const CREDENTIALS_FILE = "credentials.json";
|
|
15
|
+
const CONFIG_DIR_ENV = "DAEMON_CONFIG_DIR";
|
|
15
16
|
|
|
16
17
|
/** Keys that belong in credentials.json (secrets) vs preferences.json (settings) */
|
|
17
18
|
const CREDENTIAL_KEYS = ["openRouterApiKey", "openAiApiKey", "exaApiKey"] as const;
|
|
@@ -32,6 +33,8 @@ function getBaseConfigDir(): string {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function getAppConfigDir(): string {
|
|
36
|
+
const override = process.env[CONFIG_DIR_ENV]?.trim();
|
|
37
|
+
if (override) return override;
|
|
35
38
|
return path.join(getBaseConfigDir(), APP_DIR_NAME);
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -8,6 +8,10 @@ function normalizeWhitespace(text: string): string {
|
|
|
8
8
|
return text.replace(/\r\n/g, "\n").replace(/\t/g, " ");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function escapeXmlAttribute(value: string): string {
|
|
12
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
function truncateText(text: string, maxChars: number): { text: string; truncated: boolean } {
|
|
12
16
|
if (text.length <= maxChars) return { text, truncated: false };
|
|
13
17
|
if (maxChars <= 1) return { text: "…", truncated: true };
|
|
@@ -61,6 +65,10 @@ type ExaLikeItem = {
|
|
|
61
65
|
text?: unknown;
|
|
62
66
|
lineOffset?: unknown;
|
|
63
67
|
lineLimit?: unknown;
|
|
68
|
+
success?: unknown;
|
|
69
|
+
error?: unknown;
|
|
70
|
+
remainingLines?: unknown;
|
|
71
|
+
totalLines?: unknown;
|
|
64
72
|
};
|
|
65
73
|
|
|
66
74
|
function extractExaItems(data: unknown): ExaLikeItem[] | null {
|
|
@@ -105,43 +113,124 @@ function formatExaSearchResult(result: unknown): string | null {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
function formatExaFetchResult(result: unknown): string | null {
|
|
116
|
+
if (typeof result === "string") {
|
|
117
|
+
const trimmed = result.trim();
|
|
118
|
+
if (!trimmed) return null;
|
|
119
|
+
if (trimmed.startsWith("<fetchUrls") && trimmed.includes("error=") && !trimmed.includes("</fetchUrls>")) {
|
|
120
|
+
return trimmed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const rawLines = trimmed
|
|
124
|
+
.split("\n")
|
|
125
|
+
.map((line) => line.trimEnd())
|
|
126
|
+
.filter((line) => line.trim().length > 0);
|
|
127
|
+
const hasFetchHeader = rawLines.some((line) => line.includes("<fetchUrls"));
|
|
128
|
+
if (!hasFetchHeader) return trimmed;
|
|
129
|
+
|
|
130
|
+
const headerLine = rawLines.find((line) => line.includes("<fetchUrls"));
|
|
131
|
+
const closingLine = rawLines.find((line) => line.includes("</fetchUrls>"));
|
|
132
|
+
const urlLines = rawLines.filter((line) => line.includes("<url"));
|
|
133
|
+
const errorLines = urlLines.filter((line) => line.includes("error="));
|
|
134
|
+
const normalLines = urlLines.filter((line) => !line.includes("error="));
|
|
135
|
+
|
|
136
|
+
let contentLine: string | undefined;
|
|
137
|
+
if (normalLines.length === 1) {
|
|
138
|
+
const firstUrlIdx = rawLines.findIndex((line) => line.includes("<url") && line.includes("href="));
|
|
139
|
+
if (firstUrlIdx >= 0) {
|
|
140
|
+
contentLine = rawLines
|
|
141
|
+
.slice(firstUrlIdx + 1)
|
|
142
|
+
.find(
|
|
143
|
+
(line) => !line.includes("</url>") && !line.includes("<url") && !line.includes("</fetchUrls>")
|
|
144
|
+
)
|
|
145
|
+
?.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lines: string[] = [];
|
|
150
|
+
if (headerLine) lines.push(headerLine);
|
|
151
|
+
if (normalLines[0]) lines.push(normalLines[0]);
|
|
152
|
+
|
|
153
|
+
const firstErrorLine = errorLines[0];
|
|
154
|
+
if (firstErrorLine) {
|
|
155
|
+
lines.push(firstErrorLine);
|
|
156
|
+
} else if (contentLine) {
|
|
157
|
+
lines.push(contentLine);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (contentLine && errorLines.length > 0 && lines.length < 4) {
|
|
161
|
+
lines.push(contentLine);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (closingLine && lines.length < 4) {
|
|
165
|
+
lines.push(closingLine);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.length > 0 ? lines.join("\n") : trimmed;
|
|
169
|
+
}
|
|
108
170
|
if (!isRecord(result)) return null;
|
|
109
171
|
if (result.success === false && typeof result.error === "string") {
|
|
110
172
|
return `error: ${result.error}`;
|
|
111
173
|
}
|
|
112
174
|
if (result.success !== true) return null;
|
|
113
175
|
const data = extractToolDataContainer(result);
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
const container = isRecord(data) ? data : {};
|
|
177
|
+
const results = Array.isArray(container.results) ? container.results : null;
|
|
178
|
+
if (!results) return null;
|
|
179
|
+
|
|
180
|
+
const lines: string[] = ["<fetchUrls>"];
|
|
181
|
+
const maxItems = 3;
|
|
182
|
+
const MAX_LINES = 2;
|
|
183
|
+
const MAX_CHARS = 160;
|
|
184
|
+
|
|
185
|
+
for (const item of results.slice(0, maxItems)) {
|
|
186
|
+
if (!isRecord(item)) continue;
|
|
187
|
+
|
|
188
|
+
const candidate = item as ExaLikeItem & {
|
|
189
|
+
remainingLines?: unknown;
|
|
190
|
+
totalLines?: unknown;
|
|
191
|
+
error?: unknown;
|
|
192
|
+
};
|
|
193
|
+
const url = typeof candidate.url === "string" ? candidate.url : "";
|
|
194
|
+
if (!url) continue;
|
|
195
|
+
|
|
196
|
+
const attributes: string[] = [`href="${escapeXmlAttribute(url)}"`];
|
|
197
|
+
if (typeof candidate.lineOffset === "number") attributes.push(`lineOffset="${candidate.lineOffset}"`);
|
|
198
|
+
if (typeof candidate.lineLimit === "number") attributes.push(`lineLimit="${candidate.lineLimit}"`);
|
|
199
|
+
if (typeof candidate.totalLines === "number") attributes.push(`totalLines="${candidate.totalLines}"`);
|
|
200
|
+
if (typeof candidate.remainingLines === "number") {
|
|
201
|
+
attributes.push(`remainingLines="${candidate.remainingLines}"`);
|
|
202
|
+
} else if (candidate.remainingLines === null) {
|
|
203
|
+
attributes.push(`remainingLines="unknown"`);
|
|
204
|
+
}
|
|
132
205
|
|
|
133
|
-
|
|
134
|
-
|
|
206
|
+
if (candidate.success === false && typeof candidate.error === "string") {
|
|
207
|
+
attributes.push(`error="${escapeXmlAttribute(candidate.error)}"`);
|
|
208
|
+
lines.push(` <url ${attributes.join(" ")} />`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
135
211
|
|
|
136
|
-
|
|
137
|
-
|
|
212
|
+
const text = typeof candidate.text === "string" ? candidate.text : "";
|
|
213
|
+
if (!text.trim()) {
|
|
214
|
+
lines.push(` <url ${attributes.join(" ")} />`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
138
217
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
218
|
+
const snippetLines = normalizeWhitespace(text)
|
|
219
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
220
|
+
.trim()
|
|
221
|
+
.split("\n")
|
|
222
|
+
.slice(0, MAX_LINES)
|
|
223
|
+
.map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
|
|
143
224
|
|
|
144
|
-
|
|
225
|
+
lines.push(` <url ${attributes.join(" ")}>`);
|
|
226
|
+
for (const line of snippetLines) {
|
|
227
|
+
lines.push(` ${escapeXmlAttribute(line)}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push(" </url>");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push("</fetchUrls>");
|
|
233
|
+
return lines.length > 2 ? lines.join("\n") : null;
|
|
145
234
|
}
|
|
146
235
|
|
|
147
236
|
function formatRenderUrlResult(result: unknown): string | null {
|