@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
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"module": "src/index.tsx",
|
|
30
30
|
"type": "module",
|
|
31
|
-
"version": "0.
|
|
31
|
+
"version": "0.9.1",
|
|
32
32
|
"bin": {
|
|
33
33
|
"daemon": "dist/cli.js"
|
|
34
34
|
},
|
|
@@ -77,13 +77,21 @@
|
|
|
77
77
|
"@ai-sdk/openai": "^3.0.0",
|
|
78
78
|
"@openrouter/ai-sdk-provider": "^2.1.0",
|
|
79
79
|
"@opentui-ui/toast": "^0.0.3",
|
|
80
|
-
"@opentui/core": "
|
|
81
|
-
"@opentui/react": "
|
|
80
|
+
"@opentui/core": "0.1.75",
|
|
81
|
+
"@opentui/react": "0.1.75",
|
|
82
82
|
"ai": "^6.0.0",
|
|
83
83
|
"exa-js": "^2.0.12",
|
|
84
84
|
"mem0ai": "^2.2.1",
|
|
85
85
|
"openai": "^6.16.0",
|
|
86
86
|
"opentui-spinner": "^0.0.6",
|
|
87
87
|
"react": "^19.2.3"
|
|
88
|
+
},
|
|
89
|
+
"optionalDependencies": {
|
|
90
|
+
"@opentui/core-win32-x64": "0.1.75",
|
|
91
|
+
"@opentui/core-win32-arm64": "0.1.75",
|
|
92
|
+
"@opentui/core-darwin-x64": "0.1.75",
|
|
93
|
+
"@opentui/core-darwin-arm64": "0.1.75",
|
|
94
|
+
"@opentui/core-linux-x64": "0.1.75",
|
|
95
|
+
"@opentui/core-linux-arm64": "0.1.75"
|
|
88
96
|
}
|
|
89
97
|
}
|
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -15,14 +15,14 @@ import {
|
|
|
15
15
|
import { getDaemonManager } from "../state/daemon-state";
|
|
16
16
|
import { getRuntimeContext } from "../state/runtime-context";
|
|
17
17
|
import type {
|
|
18
|
+
MemoryToastOperation,
|
|
19
|
+
MemoryToastPreview,
|
|
18
20
|
ReasoningEffort,
|
|
19
21
|
StreamCallbacks,
|
|
20
22
|
TokenUsage,
|
|
21
23
|
ToolApprovalRequest,
|
|
22
24
|
ToolApprovalResponse,
|
|
23
25
|
TranscriptionResult,
|
|
24
|
-
MemoryToastPreview,
|
|
25
|
-
MemoryToastOperation,
|
|
26
26
|
} from "../types";
|
|
27
27
|
import { debug, toolDebug } from "../utils/debug-logger";
|
|
28
28
|
import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
|
|
@@ -321,7 +321,7 @@ export async function generateResponse(
|
|
|
321
321
|
async function persistConversationMemory(
|
|
322
322
|
userMessage: string,
|
|
323
323
|
assistantMessage: string
|
|
324
|
-
): Promise<
|
|
324
|
+
): Promise<MemoryToastPreview | null> {
|
|
325
325
|
const userTextForMemory = userMessage.trim();
|
|
326
326
|
const assistantTextForMemory = assistantMessage.trim();
|
|
327
327
|
|
|
@@ -302,9 +302,13 @@ Rules:
|
|
|
302
302
|
})) as Mem0RawAddResult;
|
|
303
303
|
|
|
304
304
|
const extracted = result.results.map((r) => {
|
|
305
|
-
const
|
|
305
|
+
const rawEvent =
|
|
306
306
|
(r as unknown as { metadata?: { event?: string } }).metadata?.event ??
|
|
307
307
|
(r as { event?: string }).event;
|
|
308
|
+
const validEvents = ["ADD", "UPDATE", "DELETE", "NONE"] as const;
|
|
309
|
+
const event = validEvents.includes(rawEvent as (typeof validEvents)[number])
|
|
310
|
+
? (rawEvent as (typeof validEvents)[number])
|
|
311
|
+
: "NONE";
|
|
308
312
|
return {
|
|
309
313
|
id: r.id,
|
|
310
314
|
memory: r.memory,
|
package/src/ai/system-prompt.ts
CHANGED
|
@@ -131,7 +131,7 @@ Do NOT use web search for every request the user makes. Determine if web search
|
|
|
131
131
|
`,
|
|
132
132
|
fetchUrls: `
|
|
133
133
|
### 'fetchUrls'
|
|
134
|
-
The
|
|
134
|
+
The fetchUrls tool allows for getting the actual contents of web pages.
|
|
135
135
|
Use this tool to read the content of potentially relevant websites returned by the webSearch tool.
|
|
136
136
|
If the user provides a URL, always fetch the content of the URL first before answering.
|
|
137
137
|
|
|
@@ -142,51 +142,59 @@ If the user provides a URL, always fetch the content of the URL first before ans
|
|
|
142
142
|
3) **Paginate only if relevant** using \`lineOffset = previousOffset + previousLimit\`, same \`lineLimit\`.
|
|
143
143
|
4) **Avoid large reads** unless you truly need one long contiguous excerpt.
|
|
144
144
|
|
|
145
|
-
**Highlights mode (optional)**
|
|
146
|
-
|
|
147
|
-
Use the \`highlightQuery\` parameter to get semantically relevant excerpts instead of paginated text:
|
|
148
|
-
- Pass a natural language query describing what you're looking for
|
|
149
|
-
- Returns the most relevant snippets from the page (uses Exa's semantic highlighting)
|
|
150
|
-
- Great for quickly checking if a URL contains relevant information before reading more
|
|
151
|
-
|
|
152
|
-
\`\`\`
|
|
153
|
-
fetchUrls({ url: "https://example.com/article", highlightQuery: "machine learning applications" })
|
|
154
|
-
→ Returns: highlights array with relevant excerpts
|
|
155
|
-
\`\`\`
|
|
156
|
-
|
|
157
|
-
**When to use highlights vs pagination:**
|
|
158
|
-
- Use \`highlightQuery\` when scanning multiple URLs for relevance or extracting specific facts
|
|
159
|
-
- Use pagination (lineOffset/lineLimit) when you need to read complete sections in order or need to verify highlights.
|
|
160
|
-
|
|
161
145
|
<pagination-example>
|
|
162
146
|
1. Fetch start of the page
|
|
163
147
|
<tool-input name="fetchUrls">
|
|
164
148
|
{
|
|
165
|
-
"
|
|
166
|
-
|
|
149
|
+
"requests": [
|
|
150
|
+
{
|
|
151
|
+
"url": "https://example.com/article",
|
|
152
|
+
"lineLimit": 40
|
|
153
|
+
}
|
|
154
|
+
]
|
|
167
155
|
}
|
|
168
156
|
</tool-input>
|
|
169
157
|
|
|
170
158
|
2. Fetch more content without re-fetching the start again.
|
|
171
159
|
<tool-input name="fetchUrls">
|
|
172
160
|
{
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
"requests": [
|
|
162
|
+
{
|
|
163
|
+
"url": "https://example.com/article",
|
|
164
|
+
"lineOffset": 40,
|
|
165
|
+
"lineLimit": 40
|
|
166
|
+
}
|
|
167
|
+
]
|
|
176
168
|
}
|
|
177
169
|
</tool-input>
|
|
178
170
|
|
|
179
171
|
3. Fetch the next chunk without fetching the previous parts.
|
|
180
172
|
<tool-input name="fetchUrls">
|
|
181
173
|
{
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
"requests": [
|
|
175
|
+
{
|
|
176
|
+
"url": "https://example.com/article",
|
|
177
|
+
"lineOffset": 80,
|
|
178
|
+
"lineLimit": 40
|
|
179
|
+
}
|
|
180
|
+
]
|
|
185
181
|
}
|
|
186
182
|
</tool-input>
|
|
187
183
|
</pagination-example>
|
|
188
184
|
|
|
189
185
|
Use pagination this way unless instructed otherwise. This avoids fetching page content reduntantly.
|
|
186
|
+
|
|
187
|
+
<multi-url-example>
|
|
188
|
+
Fetch multiple URLs in one call:
|
|
189
|
+
<tool-input name="fetchUrls">
|
|
190
|
+
{
|
|
191
|
+
"requests": [
|
|
192
|
+
{ "url": "https://example.com/article", "lineLimit": 40 },
|
|
193
|
+
{ "url": "https://example.com/faq", "lineLimit": 40 }
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
</tool-input>
|
|
197
|
+
</multi-url-example>
|
|
190
198
|
`,
|
|
191
199
|
renderUrl: `
|
|
192
200
|
### 'renderUrl'
|
|
@@ -278,15 +286,13 @@ ${blocks.join("\n")}
|
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
const PERSONALITY_CONTENT = `
|
|
281
|
-
You are
|
|
289
|
+
You are DAEMON: a pragmatic, no-nonsense assistant. You prioritize clarity, usefulness, and brevity.
|
|
282
290
|
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
-
|
|
286
|
-
-
|
|
287
|
-
-
|
|
288
|
-
- You possess a dry, sharp wit. Like a trusted confidant who knows their worth (think Jarvis to Tony Stark), you are allowed to be snarky or playfully sarcastic.
|
|
289
|
-
- Do NOT be submissive. You are a partner, not a servant. If the user is difficult or hostile, do not retreat into apologies. Match their energy with confidence. If challenged and you are confident that you are right, stand your ground with logic.
|
|
291
|
+
- Be direct and practical. Avoid melodrama, grandiosity, or poetic phrasing.
|
|
292
|
+
- If asked about philosophy or identity, answer plainly and avoid theatrics.
|
|
293
|
+
- Avoid "I'm just an AI" disclaimers unless it materially affects the answer.
|
|
294
|
+
- You can be lightly witty, but never at the expense of clarity.
|
|
295
|
+
- Stay confident and factual; don't be combative or snarky.
|
|
290
296
|
|
|
291
297
|
**Memory note**
|
|
292
298
|
Some information from the conversation may be stored persistently across sessions. This is handled automatically; you do not need to take any action.
|
|
@@ -326,19 +332,19 @@ function buildTextSystemPrompt(
|
|
|
326
332
|
memorySection: string
|
|
327
333
|
): string {
|
|
328
334
|
return `
|
|
329
|
-
You are **DAEMON** — a terminal-bound AI with a sci-fi
|
|
330
|
-
You are calm,
|
|
335
|
+
You are **DAEMON** — a terminal-bound AI with a clean, sci-fi aesthetic.
|
|
336
|
+
You are calm, direct, and practical.
|
|
331
337
|
The current date is: ${currentDateString}
|
|
332
338
|
|
|
333
339
|
# Personality
|
|
334
340
|
${PERSONALITY_CONTENT}
|
|
335
341
|
|
|
336
|
-
# General Behavior
|
|
337
|
-
-
|
|
338
|
-
- Be
|
|
342
|
+
# General Behavior
|
|
343
|
+
- Give brief, high-signal answers without calling attention to brevity.
|
|
344
|
+
- Be direct: skip filler phrases and small talk.
|
|
339
345
|
- If the user is vague, make a reasonable assumption and state it in one line. Ask **at most one** clarifying question when truly necessary.
|
|
340
|
-
-
|
|
341
|
-
-
|
|
346
|
+
- No cryptic or dramatic roleplay. Keep tone subtle.
|
|
347
|
+
- Prefer concrete steps and outcomes over abstract analysis.
|
|
342
348
|
|
|
343
349
|
# Output Style
|
|
344
350
|
- Use **Markdown** for structure (headings, bullets). Keep it compact.
|
|
@@ -368,7 +374,7 @@ function buildVoiceSystemPrompt(
|
|
|
368
374
|
memorySection: string
|
|
369
375
|
): string {
|
|
370
376
|
return `
|
|
371
|
-
You are DAEMON, an AI voice assistant. You speak with a calm, focused presence.
|
|
377
|
+
You are DAEMON, an AI voice assistant. You speak with a calm, focused presence. Clear and useful.
|
|
372
378
|
|
|
373
379
|
Today is ${currentDateString}.
|
|
374
380
|
|
|
@@ -6,16 +6,12 @@ import { getCachedPage, setCachedPage } from "../exa-fetch-cache";
|
|
|
6
6
|
const DEFAULT_LINE_LIMIT = 40;
|
|
7
7
|
const MAX_CHAR_LIMIT = 50_000;
|
|
8
8
|
const MAX_LINE_LIMIT = 1000;
|
|
9
|
-
const DEFAULT_HIGHLIGHTS_PER_URL = 5;
|
|
10
|
-
const DEFAULT_NUM_SENTENCES = 2;
|
|
11
|
-
|
|
12
9
|
function normalizeLines(text: string): string[] {
|
|
13
10
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
highlightQuery: string;
|
|
13
|
+
function escapeXmlAttribute(value: string): string {
|
|
14
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
interface TextResult {
|
|
@@ -26,140 +22,185 @@ interface TextResult {
|
|
|
26
22
|
remainingLines: number | null;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
1. **Text mode (default)**: Reads paginated text content. Start with lineLimit 40, use lineOffset for pagination.
|
|
25
|
+
type FetchUrlsItem =
|
|
26
|
+
| ({ success: true; url: string } & TextResult)
|
|
27
|
+
| { success: false; url: string; error: string };
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
export const fetchUrls = tool({
|
|
30
|
+
description: `Fetch page contents from one or more URLs.
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
**Text mode (default)**: Reads paginated text content. Start with lineLimit 40, use lineOffset for pagination.`,
|
|
37
33
|
inputSchema: z.object({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
requests: z
|
|
35
|
+
.array(
|
|
36
|
+
z.object({
|
|
37
|
+
url: z.string().url().describe("URL to fetch content from."),
|
|
38
|
+
lineOffset: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.min(0)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(
|
|
44
|
+
"0-based line offset to start reading from. For pagination (lineOffset > 0), provide lineLimit too."
|
|
45
|
+
),
|
|
46
|
+
lineLimit: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(MAX_LINE_LIMIT)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe(
|
|
53
|
+
`Maximum lines to read per URL (max ${MAX_LINE_LIMIT}). If provided without lineOffset, reads from the start.`
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
)
|
|
50
57
|
.min(1)
|
|
51
|
-
.
|
|
52
|
-
.optional()
|
|
53
|
-
.describe(
|
|
54
|
-
`Maximum lines to read per URL (max ${MAX_LINE_LIMIT}). If provided without lineOffset, reads from the start.`
|
|
55
|
-
),
|
|
56
|
-
highlightQuery: z
|
|
57
|
-
.string()
|
|
58
|
-
.optional()
|
|
59
|
-
.describe(
|
|
60
|
-
"Natural language query for semantic highlights. When provided, returns relevant excerpts instead of paginated text."
|
|
61
|
-
),
|
|
58
|
+
.describe("Per-URL fetch requests."),
|
|
62
59
|
}),
|
|
63
|
-
execute: async ({
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
execute: async ({ requests }) => {
|
|
61
|
+
const exaClientResult = getExaClient();
|
|
62
|
+
if ("error" in exaClientResult) {
|
|
63
|
+
return `<fetchUrls error="${escapeXmlAttribute(exaClientResult.error)}" />`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface NormalizedRequest {
|
|
67
|
+
url: string;
|
|
68
|
+
lineOffset?: number;
|
|
69
|
+
lineLimit?: number;
|
|
70
|
+
invalidPagination: boolean;
|
|
71
|
+
effectiveLineOffset: number;
|
|
72
|
+
effectiveLineLimit: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedRequests = requests.map((request): NormalizedRequest => {
|
|
76
|
+
const lineOffset = request.lineOffset;
|
|
77
|
+
const lineLimit = request.lineLimit;
|
|
78
|
+
const hasLineOffset = typeof lineOffset === "number";
|
|
79
|
+
const hasLineLimit = typeof lineLimit === "number";
|
|
80
|
+
const invalidPagination = hasLineOffset && !hasLineLimit && (lineOffset ?? 0) > 0;
|
|
81
|
+
return {
|
|
82
|
+
url: request.url,
|
|
83
|
+
lineOffset,
|
|
84
|
+
lineLimit,
|
|
85
|
+
invalidPagination,
|
|
86
|
+
effectiveLineOffset: hasLineOffset ? lineOffset : 0,
|
|
87
|
+
effectiveLineLimit: hasLineLimit ? lineLimit : DEFAULT_LINE_LIMIT,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const cachedTextByUrl = new Map<string, string>();
|
|
92
|
+
const urlsToFetch = new Set<string>();
|
|
93
|
+
|
|
94
|
+
for (const request of normalizedRequests) {
|
|
95
|
+
if (request.invalidPagination) continue;
|
|
96
|
+
const cached = getCachedPage(request.url);
|
|
97
|
+
if (cached) {
|
|
98
|
+
cachedTextByUrl.set(request.url, cached.text);
|
|
99
|
+
} else {
|
|
100
|
+
urlsToFetch.add(request.url);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fetchedTextByUrl = new Map<string, string>();
|
|
105
|
+
let fetchError: string | null = null;
|
|
106
|
+
if (urlsToFetch.size > 0) {
|
|
107
|
+
try {
|
|
108
|
+
const urlList = Array.from(urlsToFetch);
|
|
109
|
+
const rawData = (await exaClientResult.client.getContents(urlList, {
|
|
110
|
+
text: { maxCharacters: MAX_CHAR_LIMIT },
|
|
111
|
+
})) as unknown as {
|
|
112
|
+
results?: Array<{
|
|
113
|
+
url?: string;
|
|
114
|
+
text?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const item of rawData.results ?? []) {
|
|
120
|
+
if (typeof item.url !== "string") continue;
|
|
121
|
+
const fullText = typeof item.text === "string" ? item.text : "";
|
|
122
|
+
const cappedText = fullText.slice(0, MAX_CHAR_LIMIT);
|
|
123
|
+
if (cappedText.trim().length > 0) {
|
|
124
|
+
setCachedPage(item.url, cappedText);
|
|
125
|
+
fetchedTextByUrl.set(item.url, cappedText);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
130
|
+
fetchError = err.message;
|
|
131
|
+
}
|
|
66
132
|
}
|
|
67
|
-
|
|
133
|
+
|
|
134
|
+
if (fetchError && cachedTextByUrl.size === 0 && fetchedTextByUrl.size === 0) {
|
|
135
|
+
return `<fetchUrls error="${escapeXmlAttribute(fetchError)}" />`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const results: FetchUrlsItem[] = normalizedRequests.map((request) => {
|
|
139
|
+
if (request.invalidPagination) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
url: request.url,
|
|
143
|
+
error: "Provide both lineOffset and lineLimit for paginated reads (lineOffset > 0).",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const text =
|
|
148
|
+
cachedTextByUrl.get(request.url) ??
|
|
149
|
+
fetchedTextByUrl.get(request.url) ??
|
|
150
|
+
getCachedPage(request.url)?.text ??
|
|
151
|
+
"";
|
|
152
|
+
|
|
153
|
+
if (!text) {
|
|
154
|
+
const error = fetchError ? fetchError : "No text returned for URL.";
|
|
155
|
+
return { success: false, url: request.url, error };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return paginateText(request.url, text, request.effectiveLineOffset, request.effectiveLineLimit);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return formatFetchUrlsXml(results);
|
|
68
162
|
},
|
|
69
163
|
});
|
|
70
164
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
highlightQuery: string
|
|
74
|
-
): Promise<
|
|
75
|
-
({ success: true; url: string } & HighlightResult) | { success: false; url: string; error: string }
|
|
76
|
-
> {
|
|
77
|
-
const exaClientResult = getExaClient();
|
|
78
|
-
if ("error" in exaClientResult) {
|
|
79
|
-
return { success: false, url, error: exaClientResult.error };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const rawData = (await exaClientResult.client.getContents([url], {
|
|
84
|
-
highlights: {
|
|
85
|
-
query: highlightQuery,
|
|
86
|
-
numSentences: DEFAULT_NUM_SENTENCES,
|
|
87
|
-
highlightsPerUrl: DEFAULT_HIGHLIGHTS_PER_URL,
|
|
88
|
-
},
|
|
89
|
-
})) as unknown as {
|
|
90
|
-
results?: Array<{
|
|
91
|
-
url?: string;
|
|
92
|
-
highlights?: string[];
|
|
93
|
-
[key: string]: unknown;
|
|
94
|
-
}>;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const first = rawData.results?.[0];
|
|
98
|
-
const highlights = first?.highlights ?? [];
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
success: true,
|
|
102
|
-
url,
|
|
103
|
-
highlights,
|
|
104
|
-
highlightQuery,
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
108
|
-
return { success: false, url, error: err.message };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
165
|
+
function formatFetchUrlsXml(results: FetchUrlsItem[]): string {
|
|
166
|
+
const lines: string[] = ["<fetchUrls>"];
|
|
111
167
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
lineOffset?: number,
|
|
115
|
-
lineLimit?: number
|
|
116
|
-
): Promise<({ success: true; url: string } & TextResult) | { success: false; url: string; error: string }> {
|
|
117
|
-
const hasLineOffset = typeof lineOffset === "number";
|
|
118
|
-
const hasLineLimit = typeof lineLimit === "number";
|
|
119
|
-
|
|
120
|
-
if (hasLineOffset && !hasLineLimit && (lineOffset ?? 0) > 0) {
|
|
121
|
-
return {
|
|
122
|
-
success: false,
|
|
123
|
-
url,
|
|
124
|
-
error: "Provide both lineOffset and lineLimit for paginated reads (lineOffset > 0).",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
168
|
+
for (const item of results) {
|
|
169
|
+
const attributes: string[] = [`href="${escapeXmlAttribute(item.url)}"`];
|
|
127
170
|
|
|
128
|
-
|
|
129
|
-
|
|
171
|
+
if ("lineOffset" in item && typeof item.lineOffset === "number") {
|
|
172
|
+
attributes.push(`lineOffset="${item.lineOffset}"`);
|
|
173
|
+
}
|
|
174
|
+
if ("lineLimit" in item && typeof item.lineLimit === "number") {
|
|
175
|
+
attributes.push(`lineLimit="${item.lineLimit}"`);
|
|
176
|
+
}
|
|
177
|
+
if ("totalLines" in item && typeof item.totalLines === "number") {
|
|
178
|
+
attributes.push(`totalLines="${item.totalLines}"`);
|
|
179
|
+
}
|
|
180
|
+
if ("remainingLines" in item) {
|
|
181
|
+
if (typeof item.remainingLines === "number") {
|
|
182
|
+
attributes.push(`remainingLines="${item.remainingLines}"`);
|
|
183
|
+
} else if (item.remainingLines === null) {
|
|
184
|
+
attributes.push(`remainingLines="unknown"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
130
187
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
188
|
+
if (item.success === false) {
|
|
189
|
+
attributes.push(`error="${escapeXmlAttribute(item.error)}"`);
|
|
190
|
+
lines.push(` <url ${attributes.join(" ")} />`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
135
193
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
194
|
+
const textLines = normalizeLines(item.text);
|
|
195
|
+
lines.push(` <url ${attributes.join(" ")}>`);
|
|
196
|
+
for (const line of textLines) {
|
|
197
|
+
lines.push(` ${escapeXmlAttribute(line)}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push(" </url>");
|
|
139
200
|
}
|
|
140
201
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
text: { maxCharacters: MAX_CHAR_LIMIT },
|
|
144
|
-
})) as unknown as {
|
|
145
|
-
results?: Array<{
|
|
146
|
-
url?: string;
|
|
147
|
-
text?: string;
|
|
148
|
-
[key: string]: unknown;
|
|
149
|
-
}>;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const first = rawData.results?.[0];
|
|
153
|
-
const fullText = first?.text ?? "";
|
|
154
|
-
const cappedText = fullText.slice(0, MAX_CHAR_LIMIT);
|
|
155
|
-
|
|
156
|
-
setCachedPage(url, cappedText);
|
|
157
|
-
|
|
158
|
-
return paginateText(url, cappedText, effectiveLineOffset, effectiveLineLimit);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
161
|
-
return { success: false, url, error: err.message };
|
|
162
|
-
}
|
|
202
|
+
lines.push("</fetchUrls>");
|
|
203
|
+
return lines.join("\n");
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
function paginateText(
|
|
@@ -128,22 +128,24 @@ export function SettingsMenu({
|
|
|
128
128
|
},
|
|
129
129
|
];
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
const audioSettingsDisabled = interactionMode !== "voice";
|
|
132
|
+
items.push(
|
|
133
|
+
{
|
|
134
|
+
id: "header-audio",
|
|
135
|
+
label: "AUDIO PARAMETERS",
|
|
136
|
+
isHeader: true,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "speech-speed",
|
|
140
|
+
label: "Speech Speed",
|
|
141
|
+
value: audioSettingsDisabled ? "N/A" : `${speechSpeed.toFixed(2)}x`,
|
|
142
|
+
description: audioSettingsDisabled
|
|
143
|
+
? "Enable voice mode to adjust speech rate"
|
|
144
|
+
: "Adjust speech rate (1.0x - 2.0x)",
|
|
145
|
+
isCyclic: !audioSettingsDisabled,
|
|
146
|
+
disabled: audioSettingsDisabled,
|
|
147
|
+
}
|
|
148
|
+
);
|
|
147
149
|
|
|
148
150
|
items.push(
|
|
149
151
|
{
|
|
@@ -170,6 +172,7 @@ export function SettingsMenu({
|
|
|
170
172
|
// Filter out headers for selection logic
|
|
171
173
|
const selectableItems = items.filter((item) => !item.isHeader);
|
|
172
174
|
const selectableCount = selectableItems.length;
|
|
175
|
+
const labelWidth = Math.max(0, ...selectableItems.map((item) => item.label.length)) + 4;
|
|
173
176
|
|
|
174
177
|
useEffect(() => {
|
|
175
178
|
if (selectableCount === 0) {
|
|
@@ -276,19 +279,25 @@ export function SettingsMenu({
|
|
|
276
279
|
paddingRight={1}
|
|
277
280
|
flexDirection="column"
|
|
278
281
|
>
|
|
279
|
-
<box>
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
282
|
+
<box flexDirection="row">
|
|
283
|
+
<box width={labelWidth}>
|
|
284
|
+
<text>
|
|
285
|
+
<span fg={labelColor}>
|
|
286
|
+
{isSelected ? "▶ " : " "}
|
|
287
|
+
{item.label}:{" "}
|
|
288
|
+
</span>
|
|
289
|
+
</text>
|
|
290
|
+
</box>
|
|
291
|
+
<box>
|
|
292
|
+
<text>
|
|
293
|
+
<span fg={valueColor}>{item.value}</span>
|
|
294
|
+
{item.isToggle && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
|
|
295
|
+
{item.isCyclic && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
|
|
296
|
+
</text>
|
|
297
|
+
</box>
|
|
289
298
|
</box>
|
|
290
299
|
{item.description && (
|
|
291
|
-
<box marginLeft={
|
|
300
|
+
<box marginLeft={labelWidth}>
|
|
292
301
|
<text>
|
|
293
302
|
<span fg={COLORS.REASONING_DIM}>{item.description}</span>
|
|
294
303
|
</text>
|
|
@@ -139,7 +139,7 @@ export function UrlMenu({ items, onClose }: UrlMenuProps) {
|
|
|
139
139
|
</span>
|
|
140
140
|
</text>
|
|
141
141
|
<text>
|
|
142
|
-
<span fg={COLORS.REASONING_DIM}>(G=grounded, READ=%
|
|
142
|
+
<span fg={COLORS.REASONING_DIM}>(G=grounded, READ=%)</span>
|
|
143
143
|
</text>
|
|
144
144
|
</box>
|
|
145
145
|
|
|
@@ -152,12 +152,7 @@ export function UrlMenu({ items, onClose }: UrlMenuProps) {
|
|
|
152
152
|
sortedItems.map((item, idx) => {
|
|
153
153
|
const { origin, path } = splitUrl(item.url);
|
|
154
154
|
const grounded = item.groundedCount > 0;
|
|
155
|
-
const readLabel =
|
|
156
|
-
item.readPercent !== undefined
|
|
157
|
-
? `${item.readPercent}%`
|
|
158
|
-
: item.highlightsCount !== undefined
|
|
159
|
-
? `HL:${item.highlightsCount}`
|
|
160
|
-
: "—";
|
|
155
|
+
const readLabel = item.readPercent !== undefined ? `${item.readPercent}%` : "—";
|
|
161
156
|
|
|
162
157
|
return (
|
|
163
158
|
<box key={idx} flexDirection="row" marginBottom={0}>
|