@devang0907/agent-dev 0.1.4 → 0.1.5
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/dist/agent/loop.js +2 -3
- package/dist/agent/platform.js +2 -2
- package/dist/agent/tools/read.js +2 -2
- package/dist/agent/tools/search.js +130 -89
- package/dist/agent/tools/shell.js +75 -7
- package/dist/providers/openai-compat.js +34 -31
- package/dist/ui/App.js +10 -3
- package/dist/ui/ChatView.js +7 -9
- package/dist/ui/format-tool.d.ts +2 -0
- package/dist/ui/format-tool.js +25 -0
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -6,9 +6,8 @@ const MAX_TOOL_ROUNDS = 6;
|
|
|
6
6
|
const MAX_SAME_TOOL_CALLS = 2;
|
|
7
7
|
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, bash, and web_search.
|
|
8
8
|
When the user asks you to create or modify files, call write or edit once with the full file content, then reply briefly to confirm.
|
|
9
|
-
Use web_search
|
|
10
|
-
Shell commands via bash require user approval
|
|
11
|
-
Never run long-lived dev servers (npm run dev, npm start) — they time out. Use npm run build to verify, then tell the user how to start the dev server locally.
|
|
9
|
+
Use web_search for news and current events. When headlines are returned, list them as a numbered list using the exact titles — do not give vague category summaries.
|
|
10
|
+
Shell commands via bash require user approval. Dev servers (npm run dev, npm start) run in the background and return a URL.
|
|
12
11
|
Do NOT call the same tool repeatedly with the same arguments. One successful write is enough.
|
|
13
12
|
When calling tools, use the function-calling API with valid JSON arguments only (e.g. web_search: {"query": "search terms"}).
|
|
14
13
|
|
package/dist/agent/platform.js
CHANGED
|
@@ -65,10 +65,10 @@ export function getPlatformContext() {
|
|
|
65
65
|
if (platform() === "win32") {
|
|
66
66
|
lines.push("This agent runs on Windows. Use PowerShell-compatible commands.", shell.supportsAndAnd
|
|
67
67
|
? "You may chain commands with ; or &&."
|
|
68
|
-
: "Chain commands with ; (&& is NOT supported in Windows PowerShell 5).", "Examples: New-Item -ItemType Directory -Force todo-app; Set-Location todo-app", "For npx/npm scaffolding, always use non-interactive flags (--yes, -y, --defaults) and set CI=1.", "Do not use mkdir -p, rm -rf, or touch — use PowerShell equivalents or the write tool.", "
|
|
68
|
+
: "Chain commands with ; (&& is NOT supported in Windows PowerShell 5).", "Examples: New-Item -ItemType Directory -Force todo-app; Set-Location todo-app", "For npx/npm scaffolding, always use non-interactive flags (--yes, -y, --defaults) and set CI=1.", "Do not use mkdir -p, rm -rf, or touch — use PowerShell equivalents or the write tool.", "Dev servers (npm run dev, next dev) start in the background via bash and return a localhost URL.", "Do not run npm audit fix unless the user explicitly asks.");
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
71
|
-
lines.push("Use bash/sh syntax. Chain commands with && or ;.", "For npx/npm scaffolding, use non-interactive flags (--yes, -y) to avoid prompts.", "
|
|
71
|
+
lines.push("Use bash/sh syntax. Chain commands with && or ;.", "For npx/npm scaffolding, use non-interactive flags (--yes, -y) to avoid prompts.", "Dev servers (npm run dev) start in the background via bash and return a localhost URL.");
|
|
72
72
|
}
|
|
73
73
|
return lines.join("\n");
|
|
74
74
|
}
|
package/dist/agent/tools/read.js
CHANGED
|
@@ -85,8 +85,8 @@ export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
|
85
85
|
export const bashTool = {
|
|
86
86
|
name: "bash",
|
|
87
87
|
description: osPlatform() === "win32"
|
|
88
|
-
? "Run a PowerShell command
|
|
89
|
-
: "Run a bash
|
|
88
|
+
? "Run a PowerShell command. Dev servers (npm run dev) start in background. Chain with ; on Windows PowerShell 5."
|
|
89
|
+
: "Run a bash command. Dev servers (npm run dev) start in background and return a URL.",
|
|
90
90
|
parameters: {
|
|
91
91
|
type: "object",
|
|
92
92
|
properties: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const webSearchTool = {
|
|
2
2
|
name: "web_search",
|
|
3
|
-
description: "Search the internet for
|
|
3
|
+
description: "Search the internet for news, documentation, or current information",
|
|
4
4
|
parameters: {
|
|
5
5
|
type: "object",
|
|
6
6
|
properties: {
|
|
@@ -11,6 +11,19 @@ export const webSearchTool = {
|
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
14
|
+
const NEWS_QUERY_RE = /\b(news|headlines|breaking news|top\s+\d+\s+(news|stories|headlines)|\d+\s+news|today'?s?\s+(news|headlines|\d+\s+news))\b/i;
|
|
15
|
+
function isNewsQuery(query) {
|
|
16
|
+
return NEWS_QUERY_RE.test(query);
|
|
17
|
+
}
|
|
18
|
+
function headlineLimit(query) {
|
|
19
|
+
const top = query.match(/\btop\s+(\d+)\b/i);
|
|
20
|
+
if (top)
|
|
21
|
+
return Math.min(50, Math.max(1, parseInt(top[1], 10)));
|
|
22
|
+
const n = query.match(/\b(\d+)\s+news\b/i);
|
|
23
|
+
if (n)
|
|
24
|
+
return Math.min(50, Math.max(1, parseInt(n[1], 10)));
|
|
25
|
+
return 20;
|
|
26
|
+
}
|
|
14
27
|
function stripHtml(text) {
|
|
15
28
|
return decodeHtmlEntities(text.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
|
|
16
29
|
}
|
|
@@ -24,6 +37,48 @@ function decodeHtmlEntities(text) {
|
|
|
24
37
|
.replace(/'/g, "'")
|
|
25
38
|
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
26
39
|
}
|
|
40
|
+
function extractXmlTag(block, tag) {
|
|
41
|
+
const cdata = block.match(new RegExp(`<${tag}><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`))?.[1];
|
|
42
|
+
if (cdata)
|
|
43
|
+
return cdata.trim();
|
|
44
|
+
return block.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`))?.[1]?.trim() ?? "";
|
|
45
|
+
}
|
|
46
|
+
function parseRssItems(xml, limit) {
|
|
47
|
+
const results = [];
|
|
48
|
+
const itemRe = /<item>([\s\S]*?)<\/item>/gi;
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = itemRe.exec(xml)) !== null && results.length < limit) {
|
|
51
|
+
const block = match[1];
|
|
52
|
+
const title = stripHtml(extractXmlTag(block, "title"));
|
|
53
|
+
const url = extractXmlTag(block, "link");
|
|
54
|
+
const pubDate = extractXmlTag(block, "pubDate");
|
|
55
|
+
const snippet = stripHtml(extractXmlTag(block, "description")).slice(0, 200);
|
|
56
|
+
if (title && url) {
|
|
57
|
+
results.push({ title, url, snippet, pubDate });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
async function fetchGoogleNewsHeadlines(limit) {
|
|
63
|
+
const url = "https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en";
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
headers: { "User-Agent": USER_AGENT },
|
|
66
|
+
signal: AbortSignal.timeout(15000),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`Google News RSS failed (${res.status})`);
|
|
70
|
+
return parseRssItems(await res.text(), limit);
|
|
71
|
+
}
|
|
72
|
+
async function fetchGoogleNewsSearch(query, limit) {
|
|
73
|
+
const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
|
|
74
|
+
const res = await fetch(url, {
|
|
75
|
+
headers: { "User-Agent": USER_AGENT },
|
|
76
|
+
signal: AbortSignal.timeout(15000),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`Google News search RSS failed (${res.status})`);
|
|
80
|
+
return parseRssItems(await res.text(), limit);
|
|
81
|
+
}
|
|
27
82
|
function extractAttr(tag, attr) {
|
|
28
83
|
const re = new RegExp(`${attr}=['"]([^'"]*)['"]`, "i");
|
|
29
84
|
return tag.match(re)?.[1] ?? null;
|
|
@@ -41,38 +96,31 @@ function decodeDdgUrl(href) {
|
|
|
41
96
|
return href;
|
|
42
97
|
}
|
|
43
98
|
}
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
return
|
|
99
|
+
function isAdOrJunk(result) {
|
|
100
|
+
const url = result.url.toLowerCase();
|
|
101
|
+
return (url.includes("ad_provider") ||
|
|
102
|
+
url.includes("ad_domain") ||
|
|
103
|
+
url.includes("duckduckgo.com/y.") ||
|
|
104
|
+
url.length > 280);
|
|
47
105
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let match;
|
|
53
|
-
while ((match = snippetRe.exec(html)) !== null) {
|
|
54
|
-
snippets.push(stripHtml(match[1]));
|
|
106
|
+
function cleanResultUrl(url) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = new URL(url);
|
|
109
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
55
110
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const tag = linkTags[i];
|
|
59
|
-
const href = extractAttr(tag, "href");
|
|
60
|
-
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
61
|
-
if (href && title) {
|
|
62
|
-
results.push({
|
|
63
|
-
title,
|
|
64
|
-
url: decodeDdgUrl(href),
|
|
65
|
-
snippet: snippets[i] ?? "",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
111
|
+
catch {
|
|
112
|
+
return url.length > 120 ? url.slice(0, 120) + "…" : url;
|
|
68
113
|
}
|
|
69
|
-
|
|
114
|
+
}
|
|
115
|
+
function parseTagsByClass(html, className) {
|
|
116
|
+
const re = new RegExp(`<a\\b[^>]*class=['"]${className}['"][^>]*>[\\s\\S]*?<\\/a>`, "gi");
|
|
117
|
+
return html.match(re) ?? [];
|
|
70
118
|
}
|
|
71
119
|
function parseDdgHtmlResults(html) {
|
|
72
120
|
const linkTags = parseTagsByClass(html, "result__a");
|
|
73
121
|
const snippetTags = parseTagsByClass(html, "result__snippet");
|
|
74
122
|
const results = [];
|
|
75
|
-
for (let i = 0; i < linkTags.length && results.length <
|
|
123
|
+
for (let i = 0; i < linkTags.length && results.length < 15; i++) {
|
|
76
124
|
const tag = linkTags[i];
|
|
77
125
|
const href = extractAttr(tag, "href");
|
|
78
126
|
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
@@ -80,26 +128,12 @@ function parseDdgHtmlResults(html) {
|
|
|
80
128
|
const snippet = snippetTag
|
|
81
129
|
? stripHtml(snippetTag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""))
|
|
82
130
|
: "";
|
|
83
|
-
if (href && title) {
|
|
84
|
-
results.push({ title, url: decodeDdgUrl(href), snippet });
|
|
131
|
+
if (href && title && !isAdOrJunk({ title, url: decodeDdgUrl(href), snippet })) {
|
|
132
|
+
results.push({ title, url: cleanResultUrl(decodeDdgUrl(href)), snippet });
|
|
85
133
|
}
|
|
86
134
|
}
|
|
87
135
|
return results;
|
|
88
136
|
}
|
|
89
|
-
async function fetchDdgLiteResults(query) {
|
|
90
|
-
const res = await fetch("https://lite.duckduckgo.com/lite/", {
|
|
91
|
-
method: "POST",
|
|
92
|
-
headers: {
|
|
93
|
-
"User-Agent": USER_AGENT,
|
|
94
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
95
|
-
},
|
|
96
|
-
body: new URLSearchParams({ q: query }),
|
|
97
|
-
signal: AbortSignal.timeout(15000),
|
|
98
|
-
});
|
|
99
|
-
if (!res.ok)
|
|
100
|
-
throw new Error(`DuckDuckGo lite failed (${res.status})`);
|
|
101
|
-
return parseDdgLiteHtml(await res.text());
|
|
102
|
-
}
|
|
103
137
|
async function fetchDdgHtmlResults(query) {
|
|
104
138
|
const res = await fetch("https://html.duckduckgo.com/html/", {
|
|
105
139
|
method: "POST",
|
|
@@ -107,80 +141,87 @@ async function fetchDdgHtmlResults(query) {
|
|
|
107
141
|
"User-Agent": USER_AGENT,
|
|
108
142
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
109
143
|
},
|
|
110
|
-
body: new URLSearchParams({ q: query, b: "", kl: "" }),
|
|
144
|
+
body: new URLSearchParams({ q: query, b: "", kl: "us-en" }),
|
|
111
145
|
signal: AbortSignal.timeout(15000),
|
|
112
146
|
});
|
|
113
147
|
if (!res.ok)
|
|
114
148
|
throw new Error(`DuckDuckGo html failed (${res.status})`);
|
|
115
149
|
return parseDdgHtmlResults(await res.text());
|
|
116
150
|
}
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const res = await fetch(url, {
|
|
126
|
-
headers: { "User-Agent": "agent-dev/1.0" },
|
|
127
|
-
signal: AbortSignal.timeout(10000),
|
|
151
|
+
function dedupeResults(results) {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
return results.filter((r) => {
|
|
154
|
+
const key = r.title.toLowerCase().slice(0, 60);
|
|
155
|
+
if (seen.has(key))
|
|
156
|
+
return false;
|
|
157
|
+
seen.add(key);
|
|
158
|
+
return true;
|
|
128
159
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
parts.push(`Source: ${data.AbstractURL}`);
|
|
160
|
+
}
|
|
161
|
+
async function fetchNewsResults(query) {
|
|
162
|
+
const limit = headlineLimit(query);
|
|
163
|
+
const generic = /\b(top\s+\d+|today'?s?\s+news|news of today|news today|latest news|breaking news)\b/i.test(query);
|
|
164
|
+
let results;
|
|
165
|
+
if (generic) {
|
|
166
|
+
results = await fetchGoogleNewsHeadlines(limit);
|
|
137
167
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
parts.push(` ${topic.FirstURL}`);
|
|
143
|
-
}
|
|
144
|
-
else if ("Topics" in topic && topic.Topics) {
|
|
145
|
-
for (const sub of topic.Topics) {
|
|
146
|
-
if (sub.Text)
|
|
147
|
-
parts.push(sub.Text);
|
|
148
|
-
}
|
|
168
|
+
else {
|
|
169
|
+
results = await fetchGoogleNewsSearch(query, limit);
|
|
170
|
+
if (results.length < 5) {
|
|
171
|
+
results = [...results, ...(await fetchGoogleNewsHeadlines(limit))];
|
|
149
172
|
}
|
|
150
|
-
if (parts.length >= 6)
|
|
151
|
-
break;
|
|
152
173
|
}
|
|
153
|
-
return
|
|
174
|
+
return dedupeResults(results).slice(0, limit);
|
|
154
175
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
176
|
+
async function fetchWebResults(query) {
|
|
177
|
+
return dedupeResults(await fetchDdgHtmlResults(query)).slice(0, 12);
|
|
178
|
+
}
|
|
179
|
+
function formatNewsResults(query, items) {
|
|
180
|
+
const lines = [
|
|
181
|
+
`Headlines for: ${query}`,
|
|
182
|
+
`Source: Google News (US, ${new Date().toISOString().slice(0, 10)})`,
|
|
183
|
+
"",
|
|
184
|
+
];
|
|
185
|
+
if (items.length === 0) {
|
|
186
|
+
lines.push("No headlines found.");
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
for (const [i, item] of items.entries()) {
|
|
190
|
+
lines.push(`${i + 1}. ${item.title}`);
|
|
191
|
+
if (item.pubDate)
|
|
192
|
+
lines.push(` ${item.pubDate}`);
|
|
159
193
|
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push("Reply with a numbered list using these exact headline titles. Add a one-line note only if a headline is unclear.");
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
function formatWebResults(query, web) {
|
|
199
|
+
const lines = [`Search results for: ${query}`, ""];
|
|
160
200
|
if (web.length === 0) {
|
|
161
|
-
lines.push(
|
|
201
|
+
lines.push("No results found.");
|
|
162
202
|
return lines.join("\n");
|
|
163
203
|
}
|
|
164
|
-
lines.push("Web results:");
|
|
165
204
|
for (const [i, r] of web.entries()) {
|
|
166
205
|
lines.push(`${i + 1}. ${r.title}`);
|
|
167
206
|
lines.push(` ${r.url}`);
|
|
168
207
|
if (r.snippet)
|
|
169
|
-
lines.push(` ${r.snippet}`);
|
|
170
|
-
lines.push("");
|
|
208
|
+
lines.push(` ${r.snippet.slice(0, 160)}`);
|
|
171
209
|
}
|
|
172
|
-
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push("Summarize the key findings for the user concisely.");
|
|
212
|
+
return lines.join("\n");
|
|
173
213
|
}
|
|
174
214
|
export async function executeWebSearch(args) {
|
|
175
215
|
const query = args.query?.trim();
|
|
176
216
|
if (!query)
|
|
177
217
|
return "Error: query is required";
|
|
178
218
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
219
|
+
if (isNewsQuery(query)) {
|
|
220
|
+
const headlines = await fetchNewsResults(query);
|
|
221
|
+
return formatNewsResults(query, headlines);
|
|
222
|
+
}
|
|
223
|
+
const web = await fetchWebResults(query);
|
|
224
|
+
return formatWebResults(query, web);
|
|
184
225
|
}
|
|
185
226
|
catch (err) {
|
|
186
227
|
return `Error: search failed — ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -1,24 +1,92 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform as osPlatform } from "node:os";
|
|
2
3
|
import { getShellConfig, normalizeCommand } from "../platform.js";
|
|
3
4
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
5
|
const INSTALL_TIMEOUT_MS = 300_000;
|
|
5
|
-
const
|
|
6
|
+
const DEV_SERVER_BOOT_MS = 20_000;
|
|
7
|
+
const DEV_SERVER_RE = /\b(npm run dev|npm start|yarn dev|pnpm dev|next dev|nuxt dev|vite(\s|$)|react-scripts start)\b/i;
|
|
6
8
|
const INSTALL_RE = /\b(npm install|npm ci|yarn install|pnpm install|npx create-|npm audit)\b/i;
|
|
9
|
+
const READY_RE = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+|localhost:\d+|Local:\s*https?:\/\/[^\s]+|ready in \d|started server on|compiled successfully/i;
|
|
10
|
+
const backgroundProcesses = new Map();
|
|
7
11
|
function getCommandTimeout(command) {
|
|
8
12
|
return INSTALL_RE.test(command) ? INSTALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
9
13
|
}
|
|
10
14
|
function isDevServerCommand(command) {
|
|
11
15
|
return DEV_SERVER_RE.test(command) || /\brun dev\b/i.test(command);
|
|
12
16
|
}
|
|
17
|
+
function extractUrl(output) {
|
|
18
|
+
const http = output.match(/https?:\/\/(?:localhost|127\.0\.0\.1):\d+/i);
|
|
19
|
+
if (http)
|
|
20
|
+
return http[0];
|
|
21
|
+
const local = output.match(/localhost:\d+/i);
|
|
22
|
+
if (local)
|
|
23
|
+
return `http://${local[0]}`;
|
|
24
|
+
return "http://localhost:3000";
|
|
25
|
+
}
|
|
26
|
+
function killHint(pid) {
|
|
27
|
+
return osPlatform() === "win32"
|
|
28
|
+
? `Stop: taskkill /PID ${pid} /F`
|
|
29
|
+
: `Stop: kill ${pid}`;
|
|
30
|
+
}
|
|
31
|
+
async function runDevServerInBackground(command, workdir, shell) {
|
|
32
|
+
const normalized = normalizeCommand(command, shell);
|
|
33
|
+
const args = [...shell.args, normalized];
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let output = "";
|
|
36
|
+
let settled = false;
|
|
37
|
+
const finish = (message) => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
settled = true;
|
|
41
|
+
resolve(message);
|
|
42
|
+
};
|
|
43
|
+
const child = spawn(shell.executable, args, {
|
|
44
|
+
cwd: workdir,
|
|
45
|
+
detached: true,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
env: { ...process.env, FORCE_COLOR: "0", BROWSER: "none" },
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
if (!child.pid) {
|
|
51
|
+
finish("Error: failed to start dev server process");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const pid = child.pid;
|
|
55
|
+
backgroundProcesses.set(pid, child);
|
|
56
|
+
child.on("exit", () => {
|
|
57
|
+
backgroundProcesses.delete(pid);
|
|
58
|
+
});
|
|
59
|
+
const onData = (chunk) => {
|
|
60
|
+
output += String(chunk);
|
|
61
|
+
if (READY_RE.test(output)) {
|
|
62
|
+
clearTimeout(bootTimer);
|
|
63
|
+
child.stdout?.off("data", onData);
|
|
64
|
+
child.stderr?.off("data", onData);
|
|
65
|
+
child.unref();
|
|
66
|
+
const url = extractUrl(output);
|
|
67
|
+
finish(`Dev server started in background (PID ${pid}).\nOpen ${url}\n${killHint(pid)}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
child.stdout?.on("data", onData);
|
|
71
|
+
child.stderr?.on("data", onData);
|
|
72
|
+
child.on("error", (err) => {
|
|
73
|
+
backgroundProcesses.delete(pid);
|
|
74
|
+
finish(`Error: ${err.message}`);
|
|
75
|
+
});
|
|
76
|
+
const bootTimer = setTimeout(() => {
|
|
77
|
+
child.stdout?.off("data", onData);
|
|
78
|
+
child.stderr?.off("data", onData);
|
|
79
|
+
child.unref();
|
|
80
|
+
const url = extractUrl(output);
|
|
81
|
+
finish(`Dev server starting in background (PID ${pid}).\nLikely URL: ${url}\n${killHint(pid)}`);
|
|
82
|
+
}, DEV_SERVER_BOOT_MS);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
13
85
|
export async function executeShellCommand(command, workdir) {
|
|
14
86
|
const shell = getShellConfig();
|
|
15
87
|
const normalized = normalizeCommand(command, shell);
|
|
16
88
|
if (isDevServerCommand(normalized)) {
|
|
17
|
-
return
|
|
18
|
-
"Error: dev servers run until stopped and cannot run inside the agent.",
|
|
19
|
-
`Suggested: open a separate terminal, cd to the project, then run: ${normalized.trim()}`,
|
|
20
|
-
"To verify the project here, use a one-shot command like: npm run build",
|
|
21
|
-
].join("\n");
|
|
89
|
+
return runDevServerInBackground(normalized, workdir, shell);
|
|
22
90
|
}
|
|
23
91
|
const timeoutMs = getCommandTimeout(normalized);
|
|
24
92
|
const args = [...shell.args, normalized];
|
|
@@ -37,7 +105,7 @@ export async function executeShellCommand(command, workdir) {
|
|
|
37
105
|
});
|
|
38
106
|
const timer = setTimeout(() => {
|
|
39
107
|
child.kill();
|
|
40
|
-
resolve(`Error: command timed out after ${timeoutMs / 1000}s\n${combineOutput(stdout, stderr)}
|
|
108
|
+
resolve(`Error: command timed out after ${timeoutMs / 1000}s\n${combineOutput(stdout, stderr)}`.trim());
|
|
41
109
|
}, timeoutMs);
|
|
42
110
|
child.stdout?.on("data", (chunk) => {
|
|
43
111
|
stdout += String(chunk);
|
|
@@ -30,8 +30,21 @@ function unescapeJsonString(value) {
|
|
|
30
30
|
.replace(/\\"/g, '"')
|
|
31
31
|
.replace(/\\\\/g, "\\");
|
|
32
32
|
}
|
|
33
|
+
function stripToolArgWrapper(raw) {
|
|
34
|
+
let body = raw.trim().replace(/^[\[\]=\s]+/, "");
|
|
35
|
+
if (body.startsWith("(") && body.endsWith(")")) {
|
|
36
|
+
body = body.slice(1, -1).trim();
|
|
37
|
+
}
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
function argsFromFunctionTail(tail) {
|
|
41
|
+
const jsonIdx = tail.indexOf("{");
|
|
42
|
+
if (jsonIdx < 0)
|
|
43
|
+
return null;
|
|
44
|
+
return parseToolArguments(tail.slice(jsonIdx));
|
|
45
|
+
}
|
|
33
46
|
function parseToolArguments(raw) {
|
|
34
|
-
const body = raw
|
|
47
|
+
const body = stripToolArgWrapper(raw);
|
|
35
48
|
if (!body.startsWith("{"))
|
|
36
49
|
return null;
|
|
37
50
|
try {
|
|
@@ -54,6 +67,11 @@ function parseToolArguments(raw) {
|
|
|
54
67
|
if (queryMatch) {
|
|
55
68
|
return JSON.stringify({ query: unescapeJsonString(queryMatch[1]) });
|
|
56
69
|
}
|
|
70
|
+
const truncatedQuery = body.match(/"query"\s*:\s*"([\s\S]+)$/);
|
|
71
|
+
if (truncatedQuery) {
|
|
72
|
+
const query = unescapeJsonString(truncatedQuery[1].replace(/\\+$/, ""));
|
|
73
|
+
return JSON.stringify({ query });
|
|
74
|
+
}
|
|
57
75
|
const pathMatch = body.match(/"path"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
58
76
|
if (pathMatch) {
|
|
59
77
|
return JSON.stringify({ path: unescapeJsonString(pathMatch[1]) });
|
|
@@ -71,41 +89,26 @@ export function parseMalformedToolCalls(text) {
|
|
|
71
89
|
const results = [];
|
|
72
90
|
if (!text)
|
|
73
91
|
return results;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const name = match[1].trim();
|
|
84
|
-
const args = parseToolArguments(match[2]);
|
|
85
|
-
if (name && args)
|
|
86
|
-
pushRecovered(results, name, args);
|
|
87
|
-
}
|
|
88
|
-
if (results.length > 0)
|
|
89
|
-
break;
|
|
92
|
+
const tryAdd = (name, tail) => {
|
|
93
|
+
const args = argsFromFunctionTail(tail);
|
|
94
|
+
if (name && args)
|
|
95
|
+
pushRecovered(results, name, args);
|
|
96
|
+
};
|
|
97
|
+
const closedRe = /<function=([a-zA-Z0-9_]+)([\s\S]*?)<\/function>/gi;
|
|
98
|
+
let match;
|
|
99
|
+
while ((match = closedRe.exec(text)) !== null) {
|
|
100
|
+
tryAdd(match[1].trim(), match[2]);
|
|
90
101
|
}
|
|
91
102
|
if (results.length === 0) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const name = match[1].trim();
|
|
96
|
-
const args = parseToolArguments(match[2]);
|
|
97
|
-
if (name && args)
|
|
98
|
-
pushRecovered(results, name, args);
|
|
103
|
+
const truncatedRe = /<function=([a-zA-Z0-9_]+)([\s\S]+)/gi;
|
|
104
|
+
while ((match = truncatedRe.exec(text)) !== null) {
|
|
105
|
+
tryAdd(match[1].trim(), match[2]);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
if (results.length === 0) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const name = match[1].trim();
|
|
106
|
-
const args = parseToolArguments(match[2]);
|
|
107
|
-
if (name && args)
|
|
108
|
-
pushRecovered(results, name, args);
|
|
109
|
+
const toolCallRe = /<tool_call>\s*([a-zA-Z0-9_]+)([\s\S]*?)<\/tool_call>/gi;
|
|
110
|
+
while ((match = toolCallRe.exec(text)) !== null) {
|
|
111
|
+
tryAdd(match[1].trim(), match[2]);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
return results;
|
package/dist/ui/App.js
CHANGED
|
@@ -13,6 +13,7 @@ import { CommandApprovalPrompt } from "./CommandApprovalPrompt.js";
|
|
|
13
13
|
import { StartupBanner } from "./StartupBanner.js";
|
|
14
14
|
import { getTheme } from "./theme.js";
|
|
15
15
|
import { scrollViewportToBottom } from "./scroll.js";
|
|
16
|
+
import { formatToolForDisplay } from "./format-tool.js";
|
|
16
17
|
let nextMessageId = 0;
|
|
17
18
|
function toDisplayMessage(role, content, toolName) {
|
|
18
19
|
return { id: nextMessageId++, role, content, toolName };
|
|
@@ -109,13 +110,19 @@ export function App({ session, workdir, onQuit }) {
|
|
|
109
110
|
case "tool_result":
|
|
110
111
|
setDisplayMessages((prev) => [
|
|
111
112
|
...prev,
|
|
112
|
-
toDisplayMessage("tool", event.result, event.name),
|
|
113
|
+
toDisplayMessage("tool", formatToolForDisplay(event.name, event.result), event.name),
|
|
113
114
|
]);
|
|
114
115
|
break;
|
|
115
116
|
case "turn_end":
|
|
116
117
|
const final = streamingRef.current;
|
|
117
118
|
if (final) {
|
|
118
|
-
setDisplayMessages((prev) =>
|
|
119
|
+
setDisplayMessages((prev) => {
|
|
120
|
+
const last = prev[prev.length - 1];
|
|
121
|
+
if (last?.role === "assistant" && last.content.trim() === final.trim()) {
|
|
122
|
+
return prev;
|
|
123
|
+
}
|
|
124
|
+
return [...prev, toDisplayMessage("assistant", final)];
|
|
125
|
+
});
|
|
119
126
|
}
|
|
120
127
|
streamingRef.current = "";
|
|
121
128
|
setStreamingText("");
|
|
@@ -199,7 +206,7 @@ export function App({ session, workdir, onQuit }) {
|
|
|
199
206
|
await session.prompt(value);
|
|
200
207
|
}, [session, running, onQuit, exit, model, settings, openApiKeyPrompt]);
|
|
201
208
|
const hasChat = displayMessages.length > 0 || streamingText.length > 0;
|
|
202
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 2, marginBottom: 1, flexShrink: 0, children: _jsx(StartupBanner, { theme: theme
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", children: [!hasChat && (_jsx(Box, { paddingX: 2, marginBottom: 1, flexShrink: 0, children: _jsx(StartupBanner, { theme: theme }) })), _jsx(ChatView, { messages: displayMessages, theme: theme, model: model, streamingText: streamingText, running: running, autoFollow: autoFollow }), _jsx(Footer, { workdir: workdir, model: model, theme: theme }), overlay === "none" && (_jsx(Box, { flexShrink: 0, children: _jsx(Editor, { theme: theme, model: model, disabled: running, running: running, onSubmit: handleSubmit, onPauseFollow: () => {
|
|
203
210
|
autoFollowRef.current = false;
|
|
204
211
|
setAutoFollow(false);
|
|
205
212
|
} }) })), overlay === "model" && (_jsx(ModelSelector, { theme: theme, settings: settings, filter: modelFilter, onSelect: (m) => {
|
package/dist/ui/ChatView.js
CHANGED
|
@@ -3,19 +3,16 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Box, Static, Text } from "ink";
|
|
4
4
|
import { SPINNER_FRAMES, TOOL_ICONS } from "./theme.js";
|
|
5
5
|
import { LeftBorder } from "./LeftBorder.js";
|
|
6
|
-
import { Panel } from "./Panel.js";
|
|
7
6
|
import { modelRef } from "../config/models.js";
|
|
8
|
-
function
|
|
9
|
-
return text.length > max ? text.slice(0, max) + "…" : text;
|
|
10
|
-
}
|
|
11
|
-
function StaticMessage({ msg, theme, model, }) {
|
|
7
|
+
function StaticMessage({ msg, theme, model, showModelTag, }) {
|
|
12
8
|
if (msg.role === "user") {
|
|
13
|
-
return (_jsx(Box, {
|
|
9
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(LeftBorder, { theme: theme, borderColor: theme.primary, marginBottom: 0, children: _jsx(Text, { color: theme.text, children: msg.content }) }) }));
|
|
14
10
|
}
|
|
15
11
|
if (msg.role === "assistant") {
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: msg.content || "" }), _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: "\u25A3 " }), modelRef(model)] })] }));
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: msg.content || "" }), showModelTag && (_jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: "\u25A3 " }), modelRef(model)] }))] }));
|
|
17
13
|
}
|
|
18
|
-
|
|
14
|
+
const icon = TOOL_ICONS[msg.toolName ?? ""] ?? "·";
|
|
15
|
+
return (_jsx(Box, { paddingLeft: 1, marginBottom: 0, children: _jsxs(Text, { color: theme.textMuted, children: [icon, " ", msg.content] }) }));
|
|
19
16
|
}
|
|
20
17
|
export function ChatView({ messages, theme, model, streamingText, running, autoFollow = true, }) {
|
|
21
18
|
const [spinIdx, setSpinIdx] = useState(0);
|
|
@@ -29,6 +26,7 @@ export function ChatView({ messages, theme, model, streamingText, running, autoF
|
|
|
29
26
|
if (!hasContent) {
|
|
30
27
|
return null;
|
|
31
28
|
}
|
|
29
|
+
const lastAssistantId = [...messages].reverse().find((m) => m.role === "assistant")?.id;
|
|
32
30
|
const showWorking = running && !streamingText && messages.length > 0;
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Static, { items: messages, children: (msg) => (_jsx(StaticMessage, { msg: msg, theme: theme, model: model
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", marginX: 2, marginBottom: 1, children: [_jsx(Static, { items: messages, children: (msg) => (_jsx(StaticMessage, { msg: msg, theme: theme, model: model, showModelTag: msg.role === "assistant" && msg.id === lastAssistantId && !running }, msg.id)) }), streamingText && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: 1, children: [_jsx(Text, { color: theme.text, children: streamingText }), _jsxs(Text, { color: theme.textMuted, children: [_jsxs(Text, { color: theme.primary, children: [SPINNER_FRAMES[spinIdx], " "] }), !autoFollow && _jsx(Text, { color: theme.warning, children: "follow paused \u00B7 Ctrl+G " })] })] })), showWorking && (_jsx(Box, { paddingLeft: 1, marginTop: 1, children: _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " working\u2026"] }) }))] }));
|
|
34
32
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Compact one-line labels for tool activity in the chat UI. */
|
|
2
|
+
export function formatToolForDisplay(toolName, result) {
|
|
3
|
+
if (toolName === "web_search") {
|
|
4
|
+
const query = result.match(/Headlines for:\s*(.+)/)?.[1]?.trim()
|
|
5
|
+
?? result.match(/Search results for:\s*(.+)/)?.[1]?.trim();
|
|
6
|
+
return query ? `news: "${query}"` : "searched the web";
|
|
7
|
+
}
|
|
8
|
+
if (toolName === "read") {
|
|
9
|
+
return result.startsWith("Error:") ? result : "read file";
|
|
10
|
+
}
|
|
11
|
+
if (toolName === "write" || toolName === "edit") {
|
|
12
|
+
return result.startsWith("Error:") ? result : result.split("\n")[0] ?? toolName;
|
|
13
|
+
}
|
|
14
|
+
if (toolName === "bash") {
|
|
15
|
+
if (result.includes("Dev server")) {
|
|
16
|
+
const first = result.split("\n").slice(0, 2).join(" · ");
|
|
17
|
+
return first.length > 100 ? first.slice(0, 100) + "…" : first;
|
|
18
|
+
}
|
|
19
|
+
if (result.startsWith("Error:"))
|
|
20
|
+
return result.split("\n")[0] ?? result;
|
|
21
|
+
const line = result.split("\n").find((l) => l.trim()) ?? "command finished";
|
|
22
|
+
return line.length > 80 ? line.slice(0, 80) + "…" : line;
|
|
23
|
+
}
|
|
24
|
+
return result.length > 100 ? result.slice(0, 100) + "…" : result;
|
|
25
|
+
}
|