@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.
@@ -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 when you need current information, documentation, or facts from the internet.
10
- Shell commands via bash require user approval before they run propose the exact command you need.
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
 
@@ -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.", "Never run dev servers (npm run dev, npm start, next dev) via bash they run forever and will time out.", "To verify a web app, use npm run build. Tell the user to run the dev server in their own terminal.", "Do not run npm audit fix unless the user explicitly asks.");
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.", "Never run dev servers (npm run dev, npm start) via bash they run forever.", "To verify a web app, use npm run build. Tell the user to run the dev server separately.");
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
  }
@@ -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 in the project directory (Windows). Chain with ; not &&. Use non-interactive flags for npx/npm."
89
- : "Run a bash shell command in the project directory",
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 current information, documentation, or news",
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(/&#39;/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 parseTagsByClass(html, className) {
45
- const re = new RegExp(`<a\\b[^>]*class=['"]${className}['"][^>]*>[\\s\\S]*?<\\/a>`, "gi");
46
- return html.match(re) ?? [];
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 parseDdgLiteHtml(html) {
49
- const linkTags = parseTagsByClass(html, "result-link");
50
- const snippetRe = /<td[^>]*class=['"]result-snippet['"][^>]*>([\s\S]*?)<\/td>/gi;
51
- const snippets = [];
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
- const results = [];
57
- for (let i = 0; i < linkTags.length && results.length < 8; i++) {
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
- return results;
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 < 8; i++) {
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
- async function fetchWebResults(query) {
118
- const lite = await fetchDdgLiteResults(query);
119
- if (lite.length > 0)
120
- return lite;
121
- return fetchDdgHtmlResults(query);
122
- }
123
- async function fetchDdgInstantAnswer(query) {
124
- const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1`;
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
- if (!res.ok)
130
- return null;
131
- const data = (await res.json());
132
- const parts = [];
133
- if (data.AbstractText) {
134
- parts.push(data.Heading ? `${data.Heading}: ${data.AbstractText}` : data.AbstractText);
135
- if (data.AbstractURL)
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
- for (const topic of data.RelatedTopics ?? []) {
139
- if ("Text" in topic && topic.Text) {
140
- parts.push(topic.Text);
141
- if (topic.FirstURL)
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 parts.length > 0 ? parts.join("\n") : null;
174
+ return dedupeResults(results).slice(0, limit);
154
175
  }
155
- function formatResults(query, instant, web) {
156
- const lines = [`Search results for: ${query}`, ""];
157
- if (instant) {
158
- lines.push("Instant answer:", instant, "");
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(instant ? "(No additional web results.)" : "No results found.");
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
- return lines.join("\n").trim();
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
- const [instant, web] = await Promise.all([
180
- fetchDdgInstantAnswer(query).catch(() => null),
181
- fetchWebResults(query),
182
- ]);
183
- return formatResults(query, instant, web);
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 DEV_SERVER_RE = /\b(npm run dev|npm start|yarn dev|pnpm dev|next dev|nuxt dev|vite(\s|$)|react-scripts start|node .*--watch)\b/i;
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)}\nTip: dev servers (npm run dev) cannot run here — use npm run build to verify, or run the dev server in your own terminal.`.trim());
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.replace(/^[\[\]\s]*/, "").trim();
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 patterns = [
75
- /<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]*?\})\s*<\/function>/gi,
76
- /<function=([a-zA-Z0-9_]+)\s*>\s*(\{[\s\S]*?\})\s*<\/function>/gi,
77
- /<tool_call>\s*([a-zA-Z0-9_]+)\s*(\{[\s\S]*?\})\s*<\/tool_call>/gi,
78
- ];
79
- for (const re of patterns) {
80
- re.lastIndex = 0;
81
- let match;
82
- while ((match = re.exec(text)) !== null) {
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 looseRe = /<function=([a-zA-Z0-9_]+)([\s\S]*?)<\/function>/gi;
93
- let match;
94
- while ((match = looseRe.exec(text)) !== null) {
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 truncatedRe = /<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]+)/gi;
103
- let match;
104
- while ((match = truncatedRe.exec(text)) !== null) {
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) => [...prev, toDisplayMessage("assistant", final)]);
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, compact: hasChat }) }), _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: () => {
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) => {
@@ -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 truncate(text, max) {
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, { flexDirection: "column", marginBottom: 1, children: _jsx(LeftBorder, { theme: theme, borderColor: theme.primary, marginBottom: 0, children: _jsx(Text, { color: theme.text, children: msg.content }) }) }));
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
- return (_jsx(Box, { paddingLeft: 1, marginBottom: 1, children: _jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.textMuted, children: TOOL_ICONS[msg.toolName ?? ""] ?? "" }), " ", _jsx(Text, { bold: true, children: msg.toolName }), _jsxs(Text, { color: theme.textMuted, children: [" ", truncate(msg.content, 500)] })] }) }));
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 }, msg.id)) }), (streamingText || showWorking) && (_jsxs(Panel, { theme: theme, borderColor: theme.border, marginBottom: 1, children: [streamingText && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: streamingText }), _jsxs(Text, { color: theme.textMuted, children: [_jsxs(Text, { color: theme.primary, children: [SPINNER_FRAMES[spinIdx], " "] }), "responding\u2026", !autoFollow && (_jsx(Text, { color: theme.warning, children: " \u00B7 paused (Ctrl+G to follow)" }))] })] })), showWorking && (_jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " working\u2026"] }) }))] }))] }));
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,2 @@
1
+ /** Compact one-line labels for tool activity in the chat UI. */
2
+ export declare function formatToolForDisplay(toolName: string, result: string): string;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devang0907/agent-dev",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Minimal terminal coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",