@makefinks/daemon 0.7.2 → 0.9.0
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 +2 -1
- package/src/ai/agent-turn-runner.ts +5 -0
- package/src/ai/daemon-ai.ts +74 -24
- package/src/ai/mcp/mcp-manager.ts +348 -0
- package/src/ai/memory/memory-manager.ts +90 -2
- package/src/ai/model-config.ts +1 -1
- package/src/ai/system-prompt.ts +47 -41
- package/src/ai/tools/fetch-urls.ts +153 -125
- package/src/ai/tools/index.ts +14 -12
- package/src/ai/tools/subagents.ts +17 -13
- package/src/app/components/AppOverlays.tsx +2 -0
- package/src/components/SettingsMenu.tsx +49 -27
- package/src/components/ToolCallView.tsx +51 -12
- package/src/components/ToolsMenu.tsx +81 -10
- 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 +142 -80
- package/src/hooks/daemon-event-handlers.ts +9 -0
- package/src/hooks/keyboard-handlers.ts +26 -11
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-controller.ts +5 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +11 -0
- package/src/hooks/use-app-settings.ts +6 -0
- package/src/hooks/use-daemon-events.ts +4 -0
- package/src/index.tsx +3 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/daemon-events.ts +2 -0
- package/src/state/daemon-state.ts +10 -0
- package/src/types/index.ts +10 -1
- package/src/utils/config.ts +33 -0
- package/src/utils/derive-url-menu-items.ts +197 -37
- package/src/utils/preferences.ts +6 -0
- package/src/utils/tool-output-preview.ts +215 -27
package/src/utils/config.ts
CHANGED
|
@@ -4,8 +4,20 @@ import { getAppConfigDir } from "./preferences";
|
|
|
4
4
|
|
|
5
5
|
const CONFIG_FILE = "config.json";
|
|
6
6
|
|
|
7
|
+
export type McpTransportType = "http" | "sse";
|
|
8
|
+
|
|
9
|
+
export interface McpServerConfig {
|
|
10
|
+
/** Optional stable id. If omitted, derived from URL. */
|
|
11
|
+
id?: string;
|
|
12
|
+
/** Transport type for the MCP server. */
|
|
13
|
+
type: McpTransportType;
|
|
14
|
+
/** MCP endpoint URL. */
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export interface ManualConfig {
|
|
8
19
|
memoryModel?: string;
|
|
20
|
+
mcpServers?: McpServerConfig[];
|
|
9
21
|
}
|
|
10
22
|
|
|
11
23
|
let cachedConfig: ManualConfig | null = null;
|
|
@@ -17,6 +29,10 @@ function getConfigPath(): string {
|
|
|
17
29
|
return path.join(getAppConfigDir(), CONFIG_FILE);
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
export function getManualConfigPath(): string {
|
|
33
|
+
return getConfigPath();
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
export function loadManualConfig(): ManualConfig {
|
|
21
37
|
const now = Date.now();
|
|
22
38
|
|
|
@@ -56,6 +72,23 @@ function parseManualConfig(raw: Record<string, unknown>): ManualConfig {
|
|
|
56
72
|
config.memoryModel = raw.memoryModel.trim();
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
if (Array.isArray(raw.mcpServers)) {
|
|
76
|
+
const servers: McpServerConfig[] = [];
|
|
77
|
+
for (const entry of raw.mcpServers) {
|
|
78
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
79
|
+
const obj = entry as Record<string, unknown>;
|
|
80
|
+
const type = obj.type;
|
|
81
|
+
const url = obj.url;
|
|
82
|
+
if (type !== "http" && type !== "sse") continue;
|
|
83
|
+
if (typeof url !== "string" || url.trim().length === 0) continue;
|
|
84
|
+
const id = typeof obj.id === "string" && obj.id.trim().length > 0 ? obj.id.trim() : undefined;
|
|
85
|
+
servers.push({ id, type, url: url.trim() });
|
|
86
|
+
}
|
|
87
|
+
if (servers.length > 0) {
|
|
88
|
+
config.mcpServers = servers;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
return config;
|
|
60
93
|
}
|
|
61
94
|
|
|
@@ -10,6 +10,80 @@ 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
|
+
const value = attr[2] ?? "";
|
|
57
|
+
attrs[key] = unescapeXmlAttribute(value);
|
|
58
|
+
}
|
|
59
|
+
const url = attrs.href;
|
|
60
|
+
if (!url) continue;
|
|
61
|
+
const lineOffset = attrs.lineOffset !== undefined ? Number(attrs.lineOffset) : undefined;
|
|
62
|
+
const lineLimit = attrs.lineLimit !== undefined ? Number(attrs.lineLimit) : undefined;
|
|
63
|
+
const totalLines = attrs.totalLines !== undefined ? Number(attrs.totalLines) : undefined;
|
|
64
|
+
let remainingLines: number | null | undefined = undefined;
|
|
65
|
+
if (attrs.remainingLines === "unknown") remainingLines = null;
|
|
66
|
+
else if (attrs.remainingLines !== undefined) remainingLines = Number(attrs.remainingLines);
|
|
67
|
+
const error = attrs.error;
|
|
68
|
+
const success = !error;
|
|
69
|
+
items.push({
|
|
70
|
+
url,
|
|
71
|
+
lineOffset: Number.isFinite(lineOffset) ? lineOffset : undefined,
|
|
72
|
+
lineLimit: Number.isFinite(lineLimit) ? lineLimit : undefined,
|
|
73
|
+
totalLines: Number.isFinite(totalLines) ? totalLines : undefined,
|
|
74
|
+
remainingLines:
|
|
75
|
+
remainingLines === null || (typeof remainingLines === "number" && Number.isFinite(remainingLines))
|
|
76
|
+
? remainingLines
|
|
77
|
+
: undefined,
|
|
78
|
+
error,
|
|
79
|
+
success,
|
|
80
|
+
});
|
|
81
|
+
match = regex.exec(result);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { items, error };
|
|
85
|
+
}
|
|
86
|
+
|
|
13
87
|
function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
|
|
14
88
|
if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
|
|
15
89
|
if (intervals.length === 0) return undefined;
|
|
@@ -43,7 +117,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
43
117
|
|
|
44
118
|
const intervalsByUrl = new Map<string, Array<[number, number]>>();
|
|
45
119
|
const totalLinesByUrl = new Map<string, number>();
|
|
46
|
-
const highlightsCountByUrl = new Map<string, number>();
|
|
47
120
|
const lastSeenIndexByUrl = new Map<string, number>();
|
|
48
121
|
const statusByUrl = new Map<string, "ok" | "error">();
|
|
49
122
|
const errorByUrl = new Map<string, string>();
|
|
@@ -57,58 +130,147 @@ export function deriveUrlMenuItems(params: {
|
|
|
57
130
|
if (block.type !== "tool") continue;
|
|
58
131
|
if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
|
|
59
132
|
|
|
60
|
-
const input = block.call.input as { url?: string } | undefined;
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
133
|
+
const input = block.call.input as { url?: string; requests?: Array<{ url?: string }> } | undefined;
|
|
134
|
+
const urls: string[] = [];
|
|
135
|
+
if (input?.url && typeof input.url === "string") {
|
|
136
|
+
urls.push(input.url);
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(input?.requests)) {
|
|
139
|
+
for (const request of input.requests) {
|
|
140
|
+
if (request?.url && typeof request.url === "string") {
|
|
141
|
+
urls.push(request.url);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (urls.length === 0) continue;
|
|
63
146
|
|
|
64
|
-
|
|
147
|
+
for (const url of urls) {
|
|
148
|
+
lastSeenIndexByUrl.set(url, blockIndex);
|
|
149
|
+
}
|
|
65
150
|
|
|
66
151
|
const result = block.result as
|
|
67
152
|
| {
|
|
153
|
+
success?: boolean;
|
|
154
|
+
error?: string;
|
|
155
|
+
results?: Array<{
|
|
156
|
+
success?: boolean;
|
|
157
|
+
url?: string;
|
|
158
|
+
lineOffset?: number;
|
|
159
|
+
lineLimit?: number;
|
|
160
|
+
totalLines?: number;
|
|
161
|
+
error?: string;
|
|
162
|
+
}>;
|
|
68
163
|
lineOffset?: number;
|
|
69
164
|
lineLimit?: number;
|
|
70
165
|
totalLines?: number;
|
|
71
|
-
highlights?: unknown[];
|
|
72
|
-
success?: boolean;
|
|
73
|
-
error?: string;
|
|
74
166
|
}
|
|
75
167
|
| undefined;
|
|
76
168
|
|
|
77
|
-
if (!result
|
|
169
|
+
if (!result) continue;
|
|
78
170
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
statusByUrl.set(url, "ok");
|
|
84
|
-
}
|
|
171
|
+
if (typeof result === "string") {
|
|
172
|
+
const parsed = parseFetchUrlsXml(result);
|
|
173
|
+
for (const item of parsed.items) {
|
|
174
|
+
const itemUrl = item.url;
|
|
85
175
|
|
|
86
|
-
|
|
87
|
-
|
|
176
|
+
if (!item.success && item.error && item.error.trim().length > 0) {
|
|
177
|
+
statusByUrl.set(itemUrl, "error");
|
|
178
|
+
errorByUrl.set(itemUrl, item.error.trim());
|
|
179
|
+
} else if (item.success) {
|
|
180
|
+
statusByUrl.set(itemUrl, "ok");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (typeof item.totalLines === "number" && item.totalLines > 0) {
|
|
184
|
+
const prev = totalLinesByUrl.get(itemUrl) ?? 0;
|
|
185
|
+
totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
typeof item.lineOffset === "number" &&
|
|
190
|
+
typeof item.lineLimit === "number" &&
|
|
191
|
+
item.lineLimit > 0 &&
|
|
192
|
+
item.lineOffset >= 0
|
|
193
|
+
) {
|
|
194
|
+
const start = item.lineOffset;
|
|
195
|
+
const end = item.lineOffset + item.lineLimit;
|
|
196
|
+
const list = intervalsByUrl.get(itemUrl) ?? [];
|
|
197
|
+
list.push([start, end]);
|
|
198
|
+
intervalsByUrl.set(itemUrl, list);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (parsed.items.length === 0 && parsed.error) {
|
|
202
|
+
for (const url of urls) {
|
|
203
|
+
statusByUrl.set(url, "error");
|
|
204
|
+
errorByUrl.set(url, parsed.error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
88
208
|
}
|
|
89
209
|
|
|
90
|
-
if (
|
|
210
|
+
if (typeof result !== "object") continue;
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(result.results)) {
|
|
213
|
+
for (const item of result.results) {
|
|
214
|
+
if (!item || typeof item !== "object") continue;
|
|
215
|
+
const itemUrl = typeof item.url === "string" ? item.url : null;
|
|
216
|
+
if (!itemUrl) continue;
|
|
217
|
+
|
|
218
|
+
if (item.success === false && typeof item.error === "string" && item.error.trim().length > 0) {
|
|
219
|
+
statusByUrl.set(itemUrl, "error");
|
|
220
|
+
errorByUrl.set(itemUrl, item.error.trim());
|
|
221
|
+
} else if (item.success === true) {
|
|
222
|
+
statusByUrl.set(itemUrl, "ok");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof item.totalLines === "number" && Number.isFinite(item.totalLines) && item.totalLines > 0) {
|
|
226
|
+
const prev = totalLinesByUrl.get(itemUrl) ?? 0;
|
|
227
|
+
totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
typeof item.lineOffset === "number" &&
|
|
232
|
+
typeof item.lineLimit === "number" &&
|
|
233
|
+
Number.isFinite(item.lineOffset) &&
|
|
234
|
+
Number.isFinite(item.lineLimit) &&
|
|
235
|
+
item.lineLimit > 0 &&
|
|
236
|
+
item.lineOffset >= 0
|
|
237
|
+
) {
|
|
238
|
+
const start = item.lineOffset;
|
|
239
|
+
const end = item.lineOffset + item.lineLimit;
|
|
240
|
+
const list = intervalsByUrl.get(itemUrl) ?? [];
|
|
241
|
+
list.push([start, end]);
|
|
242
|
+
intervalsByUrl.set(itemUrl, list);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if (
|
|
246
|
+
result.success === false &&
|
|
247
|
+
typeof result.error === "string" &&
|
|
248
|
+
result.error.trim().length > 0
|
|
249
|
+
) {
|
|
250
|
+
for (const url of urls) {
|
|
251
|
+
statusByUrl.set(url, "error");
|
|
252
|
+
errorByUrl.set(url, result.error.trim());
|
|
253
|
+
}
|
|
254
|
+
} else if (result.success === true) {
|
|
255
|
+
for (const url of urls) {
|
|
256
|
+
statusByUrl.set(url, "ok");
|
|
257
|
+
}
|
|
258
|
+
} else if (
|
|
91
259
|
typeof result.totalLines === "number" &&
|
|
92
260
|
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 (
|
|
261
|
+
result.totalLines > 0 &&
|
|
100
262
|
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
|
|
263
|
+
typeof result.lineLimit === "number"
|
|
106
264
|
) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
265
|
+
for (const url of urls) {
|
|
266
|
+
const prev = totalLinesByUrl.get(url) ?? 0;
|
|
267
|
+
totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
|
|
268
|
+
const start = result.lineOffset;
|
|
269
|
+
const end = result.lineOffset + result.lineLimit;
|
|
270
|
+
const list = intervalsByUrl.get(url) ?? [];
|
|
271
|
+
list.push([start, end]);
|
|
272
|
+
intervalsByUrl.set(url, list);
|
|
273
|
+
}
|
|
112
274
|
}
|
|
113
275
|
}
|
|
114
276
|
|
|
@@ -134,7 +296,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
134
296
|
const urls = [...lastSeenIndexByUrl.keys()];
|
|
135
297
|
return urls.map((url) => {
|
|
136
298
|
const groundedCount = lookupGroundedCount(url);
|
|
137
|
-
const highlightsCount = highlightsCountByUrl.get(url);
|
|
138
299
|
const totalLines = totalLinesByUrl.get(url);
|
|
139
300
|
const intervals = intervalsByUrl.get(url) ?? [];
|
|
140
301
|
const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
|
|
@@ -146,7 +307,6 @@ export function deriveUrlMenuItems(params: {
|
|
|
146
307
|
url,
|
|
147
308
|
groundedCount,
|
|
148
309
|
readPercent,
|
|
149
|
-
highlightsCount,
|
|
150
310
|
status,
|
|
151
311
|
error,
|
|
152
312
|
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
|
|
|
@@ -109,6 +112,9 @@ export function parsePreferences(raw: unknown): AppPreferences | null {
|
|
|
109
112
|
if (typeof raw.showToolOutput === "boolean") {
|
|
110
113
|
prefs.showToolOutput = raw.showToolOutput;
|
|
111
114
|
}
|
|
115
|
+
if (typeof raw.memoryEnabled === "boolean") {
|
|
116
|
+
prefs.memoryEnabled = raw.memoryEnabled;
|
|
117
|
+
}
|
|
112
118
|
if (isRecord(raw.toolToggles)) {
|
|
113
119
|
const record = raw.toolToggles;
|
|
114
120
|
const next: Record<string, boolean> = {};
|
|
@@ -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 };
|
|
@@ -105,43 +109,123 @@ function formatExaSearchResult(result: unknown): string | null {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
function formatExaFetchResult(result: unknown): string | null {
|
|
112
|
+
if (typeof result === "string") {
|
|
113
|
+
const trimmed = result.trim();
|
|
114
|
+
if (!trimmed) return null;
|
|
115
|
+
if (trimmed.startsWith("<fetchUrls") && trimmed.includes("error=") && !trimmed.includes("</fetchUrls>")) {
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rawLines = trimmed
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((line) => line.trimEnd())
|
|
122
|
+
.filter((line) => line.trim().length > 0);
|
|
123
|
+
const hasFetchHeader = rawLines.some((line) => line.includes("<fetchUrls"));
|
|
124
|
+
if (!hasFetchHeader) return trimmed;
|
|
125
|
+
|
|
126
|
+
const headerLine = rawLines.find((line) => line.includes("<fetchUrls"));
|
|
127
|
+
const closingLine = rawLines.find((line) => line.includes("</fetchUrls>"));
|
|
128
|
+
const urlLines = rawLines.filter((line) => line.includes("<url"));
|
|
129
|
+
const errorLines = urlLines.filter((line) => line.includes("error="));
|
|
130
|
+
const normalLines = urlLines.filter((line) => !line.includes("error="));
|
|
131
|
+
|
|
132
|
+
let contentLine: string | undefined;
|
|
133
|
+
if (normalLines.length === 1) {
|
|
134
|
+
const firstUrlIdx = rawLines.findIndex((line) => line.includes("<url") && line.includes("href="));
|
|
135
|
+
if (firstUrlIdx >= 0) {
|
|
136
|
+
contentLine = rawLines
|
|
137
|
+
.slice(firstUrlIdx + 1)
|
|
138
|
+
.find(
|
|
139
|
+
(line) => !line.includes("</url>") && !line.includes("<url") && !line.includes("</fetchUrls>")
|
|
140
|
+
)
|
|
141
|
+
?.trim();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
if (headerLine) lines.push(headerLine);
|
|
147
|
+
if (normalLines[0]) lines.push(normalLines[0]);
|
|
148
|
+
|
|
149
|
+
if (errorLines.length > 0) {
|
|
150
|
+
lines.push(errorLines[0]);
|
|
151
|
+
} else if (contentLine) {
|
|
152
|
+
lines.push(contentLine);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (contentLine && errorLines.length > 0 && lines.length < 4) {
|
|
156
|
+
lines.push(contentLine);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (closingLine && lines.length < 4) {
|
|
160
|
+
lines.push(closingLine);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return lines.length > 0 ? lines.join("\n") : trimmed;
|
|
164
|
+
}
|
|
108
165
|
if (!isRecord(result)) return null;
|
|
109
166
|
if (result.success === false && typeof result.error === "string") {
|
|
110
167
|
return `error: ${result.error}`;
|
|
111
168
|
}
|
|
112
169
|
if (result.success !== true) return null;
|
|
113
170
|
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
|
-
|
|
171
|
+
const container = isRecord(data) ? data : {};
|
|
172
|
+
const results = Array.isArray(container.results) ? container.results : null;
|
|
173
|
+
if (!results) return null;
|
|
174
|
+
|
|
175
|
+
const lines: string[] = ["<fetchUrls>"];
|
|
176
|
+
const maxItems = 3;
|
|
177
|
+
const MAX_LINES = 2;
|
|
178
|
+
const MAX_CHARS = 160;
|
|
179
|
+
|
|
180
|
+
for (const item of results.slice(0, maxItems)) {
|
|
181
|
+
if (!isRecord(item)) continue;
|
|
182
|
+
|
|
183
|
+
const candidate = item as ExaLikeItem & {
|
|
184
|
+
remainingLines?: unknown;
|
|
185
|
+
totalLines?: unknown;
|
|
186
|
+
error?: unknown;
|
|
187
|
+
};
|
|
188
|
+
const url = typeof candidate.url === "string" ? candidate.url : "";
|
|
189
|
+
if (!url) continue;
|
|
190
|
+
|
|
191
|
+
const attributes: string[] = [`href="${escapeXmlAttribute(url)}"`];
|
|
192
|
+
if (typeof candidate.lineOffset === "number") attributes.push(`lineOffset="${candidate.lineOffset}"`);
|
|
193
|
+
if (typeof candidate.lineLimit === "number") attributes.push(`lineLimit="${candidate.lineLimit}"`);
|
|
194
|
+
if (typeof candidate.totalLines === "number") attributes.push(`totalLines="${candidate.totalLines}"`);
|
|
195
|
+
if (typeof candidate.remainingLines === "number") {
|
|
196
|
+
attributes.push(`remainingLines="${candidate.remainingLines}"`);
|
|
197
|
+
} else if (candidate.remainingLines === null) {
|
|
198
|
+
attributes.push(`remainingLines="unknown"`);
|
|
199
|
+
}
|
|
132
200
|
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
if (candidate.success === false && typeof candidate.error === "string") {
|
|
202
|
+
attributes.push(`error="${escapeXmlAttribute(candidate.error)}"`);
|
|
203
|
+
lines.push(` <url ${attributes.join(" ")} />`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
135
206
|
|
|
136
|
-
|
|
137
|
-
|
|
207
|
+
const text = typeof candidate.text === "string" ? candidate.text : "";
|
|
208
|
+
if (!text.trim()) {
|
|
209
|
+
lines.push(` <url ${attributes.join(" ")} />`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
138
212
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
213
|
+
const snippetLines = normalizeWhitespace(text)
|
|
214
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
215
|
+
.trim()
|
|
216
|
+
.split("\n")
|
|
217
|
+
.slice(0, MAX_LINES)
|
|
218
|
+
.map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
|
|
143
219
|
|
|
144
|
-
|
|
220
|
+
lines.push(` <url ${attributes.join(" ")}>`);
|
|
221
|
+
for (const line of snippetLines) {
|
|
222
|
+
lines.push(` ${escapeXmlAttribute(line)}`);
|
|
223
|
+
}
|
|
224
|
+
lines.push(" </url>");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lines.push("</fetchUrls>");
|
|
228
|
+
return lines.length > 2 ? lines.join("\n") : null;
|
|
145
229
|
}
|
|
146
230
|
|
|
147
231
|
function formatRenderUrlResult(result: unknown): string | null {
|
|
@@ -194,6 +278,66 @@ function formatReadFileResult(result: unknown): string | null {
|
|
|
194
278
|
return `${path}${range}:\n${content}`;
|
|
195
279
|
}
|
|
196
280
|
|
|
281
|
+
function tryStringify(value: unknown): string {
|
|
282
|
+
if (typeof value === "string") return value;
|
|
283
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
|
|
284
|
+
return String(value);
|
|
285
|
+
if (value === null) return "null";
|
|
286
|
+
if (value === undefined) return "undefined";
|
|
287
|
+
try {
|
|
288
|
+
return JSON.stringify(value, null, 2);
|
|
289
|
+
} catch {
|
|
290
|
+
return String(value);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
type McpContentLike = {
|
|
295
|
+
type?: unknown;
|
|
296
|
+
text?: unknown;
|
|
297
|
+
content?: unknown;
|
|
298
|
+
data?: unknown;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
function extractMcpContentText(item: unknown): string {
|
|
302
|
+
if (!isRecord(item)) return "";
|
|
303
|
+
const it = item as McpContentLike;
|
|
304
|
+
if (typeof it.text === "string") return it.text;
|
|
305
|
+
if (typeof it.content === "string") return it.content;
|
|
306
|
+
if (typeof it.type === "string" && it.type === "text" && typeof it.data === "string") return it.data;
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function formatMcpLikeResult(result: unknown): string | null {
|
|
311
|
+
if (!isRecord(result)) return null;
|
|
312
|
+
|
|
313
|
+
const structuredContent =
|
|
314
|
+
"structuredContent" in result ? (result as UnknownRecord).structuredContent : undefined;
|
|
315
|
+
if (structuredContent !== undefined) {
|
|
316
|
+
const raw = tryStringify(structuredContent);
|
|
317
|
+
return raw.trim().length > 0 ? raw : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const content = "content" in result ? (result as UnknownRecord).content : undefined;
|
|
321
|
+
if (Array.isArray(content)) {
|
|
322
|
+
const pieces = content
|
|
323
|
+
.map(extractMcpContentText)
|
|
324
|
+
.map((t) => t.trim())
|
|
325
|
+
.filter((t) => t.length > 0);
|
|
326
|
+
const joined = pieces.join("\n");
|
|
327
|
+
if (joined.trim().length > 0) {
|
|
328
|
+
const isError = (result as UnknownRecord).isError === true;
|
|
329
|
+
if (isError && !joined.toLowerCase().startsWith("error:")) {
|
|
330
|
+
return `error: ${joined}`;
|
|
331
|
+
}
|
|
332
|
+
return joined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Fallback: show the raw container (truncated downstream)
|
|
337
|
+
const fallback = tryStringify(result);
|
|
338
|
+
return fallback.trim().length > 0 ? fallback : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
197
341
|
/**
|
|
198
342
|
* Format a very small, terminal-safe preview of a tool result.
|
|
199
343
|
* Intended for the tool call UI box (not full logs).
|
|
@@ -248,3 +392,47 @@ export function formatToolOutputPreview(toolName: string, result: unknown): stri
|
|
|
248
392
|
|
|
249
393
|
return outputLines.length > 0 ? outputLines : null;
|
|
250
394
|
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generic preview formatter for dynamic tools (e.g. MCP).
|
|
398
|
+
*/
|
|
399
|
+
export function formatGenericToolOutputPreview(result: unknown): string[] | null {
|
|
400
|
+
if (result === undefined) return null;
|
|
401
|
+
|
|
402
|
+
const raw = formatMcpLikeResult(result) ?? tryStringify(result);
|
|
403
|
+
if (!raw.trim()) return ["(no output)"];
|
|
404
|
+
|
|
405
|
+
const MAX_LINES = 4;
|
|
406
|
+
const MAX_CHARS_PER_LINE = 160;
|
|
407
|
+
const MAX_TOTAL_CHARS = 260;
|
|
408
|
+
|
|
409
|
+
const { lines, truncated: lineTruncated } = splitPreviewLines(raw, MAX_LINES);
|
|
410
|
+
|
|
411
|
+
let usedChars = 0;
|
|
412
|
+
const outputLines: string[] = [];
|
|
413
|
+
let anyTruncated = lineTruncated;
|
|
414
|
+
|
|
415
|
+
for (const line of lines) {
|
|
416
|
+
const remaining = Math.max(0, MAX_TOTAL_CHARS - usedChars);
|
|
417
|
+
if (remaining <= 0) {
|
|
418
|
+
anyTruncated = true;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
const { text: truncatedLine, truncated } = truncateText(line, Math.min(MAX_CHARS_PER_LINE, remaining));
|
|
422
|
+
anyTruncated = anyTruncated || truncated;
|
|
423
|
+
outputLines.push(truncatedLine);
|
|
424
|
+
usedChars += truncatedLine.length;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (outputLines.length === 0) return ["(no output)"];
|
|
428
|
+
|
|
429
|
+
if (anyTruncated && outputLines.length > 0) {
|
|
430
|
+
const last = outputLines[outputLines.length - 1] ?? "";
|
|
431
|
+
if (!last.endsWith("…")) {
|
|
432
|
+
const { text } = truncateText(last, Math.max(1, last.length - 1));
|
|
433
|
+
outputLines[outputLines.length - 1] = `${text}…`.replace(/……$/, "…");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return outputLines;
|
|
438
|
+
}
|