neoctl 0.1.7 → 0.1.9

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/web/index.js CHANGED
@@ -197,6 +197,9 @@ class WebRepl {
197
197
  status;
198
198
  busy = false;
199
199
  queuedInput;
200
+ foregroundRun;
201
+ backgroundSessionRuns = new Map();
202
+ suppressReattachedStreaming = new Set();
200
203
  backgroundTaskCount;
201
204
  constructor(runtime) {
202
205
  this.runtime = runtime;
@@ -228,6 +231,9 @@ class WebRepl {
228
231
  busy: this.busy,
229
232
  queuedInput: this.queuedInput,
230
233
  backgroundTaskCount: this.backgroundTaskCount,
234
+ backgroundTasks: this.backgroundTasks(),
235
+ backgroundSessionRunCount: this.backgroundSessionRuns.size,
236
+ runningSessionIds: [...this.backgroundSessionRuns.keys()],
231
237
  session: this.runtime.engine.snapshot().session,
232
238
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
233
239
  interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
@@ -239,39 +245,65 @@ class WebRepl {
239
245
  const trimmed = text.trim();
240
246
  if (!trimmed && attachments.length === 0)
241
247
  return { ok: true };
242
- if (this.busy) {
248
+ const command = parseReplCommand(text);
249
+ const startsNewSessionWhileBusy = this.busy && command.type === "new";
250
+ if (this.busy && !startsNewSessionWhileBusy) {
243
251
  if (this.queuedInput !== undefined)
244
252
  return { ok: false, error: "A queued prompt is already waiting. Press Esc/Ctrl+C in the web UI to clear it." };
245
253
  this.queuedInput = text;
246
254
  this.broadcastSync();
247
255
  return { ok: true };
248
256
  }
249
- void this.handleCommandOrPrompt(text, attachments).catch((error) => {
257
+ const run = this.handleCommandOrPrompt(text, attachments).catch((error) => {
250
258
  this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
251
259
  this.setBusy(false);
252
260
  this.setStatus({ ...this.status, phase: "ready", detail: undefined });
253
261
  });
262
+ this.foregroundRun = run;
263
+ run.finally(() => {
264
+ if (this.foregroundRun === run)
265
+ this.foregroundRun = undefined;
266
+ }).catch(() => undefined);
254
267
  return { ok: true };
255
268
  }
256
269
  async listSessions() {
257
- return this.runtime.engine.listSessions(Number.POSITIVE_INFINITY);
270
+ const sessions = await this.runtime.engine.listSessions(Number.POSITIVE_INFINITY);
271
+ const runningSessionIds = [...this.backgroundSessionRuns.keys()];
272
+ return { sessions, runningSessionIds };
258
273
  }
259
274
  async resumeSession(sessionId) {
260
275
  if (!sessionId)
261
276
  return { ok: false, error: "sessionId is required" };
262
277
  try {
263
- const snapshot = await this.runtime.engine.resumeSession(sessionId);
264
- const metrics = await this.runtime.engine.contextMetrics();
265
- this.runtime.usage.reset();
266
- this.status = initialStatus(this.runtime, metrics);
267
- const lineId = { current: 0 };
268
- this.lines = initialLines(this.runtime, lineId);
269
- this.lineId = lineId.current;
270
- this.assistantLineId = undefined;
271
- this.thinkingLineId = undefined;
272
- this.finalizedThinkingLineId = undefined;
273
- this.toolLineIds.clear();
274
- this.append(systemLine(formatResume(snapshot)));
278
+ const running = this.backgroundSessionRuns.get(sessionId);
279
+ if (running) {
280
+ await this.reattachRunningSession(running);
281
+ return { ok: true };
282
+ }
283
+ await this.detachRunningForeground("session switch");
284
+ this.runtime.engine = this.runtime.engine.forkForSession(sessionId, true);
285
+ await this.runtime.engine.initialize();
286
+ const snapshot = this.runtime.engine.snapshot().session;
287
+ if (!snapshot)
288
+ throw new Error("session transcripts are disabled");
289
+ await this.refreshSessionView(systemLine(formatResume(snapshot)));
290
+ return { ok: true };
291
+ }
292
+ catch (error) {
293
+ const message = error instanceof Error ? error.message : String(error);
294
+ this.append({ kind: "error", text: message });
295
+ return { ok: false, error: message };
296
+ }
297
+ }
298
+ async newSession() {
299
+ try {
300
+ await this.detachRunningForeground("new session");
301
+ this.runtime.engine = this.runtime.engine.forkForSession(undefined, false);
302
+ await this.runtime.engine.initialize();
303
+ const snapshot = this.runtime.engine.snapshot().session;
304
+ if (!snapshot)
305
+ throw new Error("session transcripts are disabled");
306
+ await this.refreshSessionView(systemLine(`new session ${snapshot.sessionId}`));
275
307
  return { ok: true };
276
308
  }
277
309
  catch (error) {
@@ -280,6 +312,21 @@ class WebRepl {
280
312
  return { ok: false, error: message };
281
313
  }
282
314
  }
315
+ async refreshSessionView(line) {
316
+ const metrics = await this.runtime.engine.contextMetrics();
317
+ this.runtime.usage.reset();
318
+ this.status = initialStatus(this.runtime, metrics);
319
+ const lineId = { current: 0 };
320
+ this.lines = initialLines(this.runtime, lineId);
321
+ this.lineId = lineId.current;
322
+ this.assistantLineId = undefined;
323
+ this.thinkingLineId = undefined;
324
+ this.finalizedThinkingLineId = undefined;
325
+ this.toolLineIds.clear();
326
+ if (line)
327
+ this.append(line);
328
+ this.broadcastSync();
329
+ }
283
330
  async deleteSession(sessionId) {
284
331
  if (!sessionId)
285
332
  return { ok: false, error: "sessionId is required" };
@@ -370,6 +417,61 @@ class WebRepl {
370
417
  this.status = next;
371
418
  this.broadcastSync();
372
419
  }
420
+ backgroundTasks() {
421
+ return this.runtime.taskStore.list()
422
+ .filter((task) => !this.runtime.taskStore.isTerminal(task))
423
+ .map((task) => ({
424
+ taskId: task.taskId,
425
+ agentId: task.agentId,
426
+ type: task.type,
427
+ status: task.status,
428
+ description: task.description,
429
+ createdAt: task.createdAt,
430
+ }));
431
+ }
432
+ async detachRunningForeground(reason) {
433
+ if (!this.busy)
434
+ return false;
435
+ const snapshot = this.runtime.engine.snapshot().session;
436
+ const sessionId = snapshot?.sessionId ?? `session-${Date.now().toString(36)}`;
437
+ const run = this.foregroundRun;
438
+ if (run && !this.backgroundSessionRuns.has(sessionId)) {
439
+ const backgroundRun = {
440
+ sessionId,
441
+ title: snapshot?.title,
442
+ reason,
443
+ startedAt: Date.now(),
444
+ engine: this.runtime.engine,
445
+ abortController: this.activeAbortController ?? new AbortController(),
446
+ promise: run,
447
+ };
448
+ this.backgroundSessionRuns.set(sessionId, backgroundRun);
449
+ run.finally(() => {
450
+ this.backgroundSessionRuns.delete(sessionId);
451
+ this.suppressReattachedStreaming.delete(backgroundRun.engine);
452
+ this.broadcastSync();
453
+ }).catch(() => undefined);
454
+ }
455
+ this.activeAbortController = undefined;
456
+ this.interruptArmed = false;
457
+ this.queuedInput = undefined;
458
+ this.busy = false;
459
+ this.status = { ...this.status, phase: "ready", detail: undefined };
460
+ this.append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
461
+ return true;
462
+ }
463
+ async reattachRunningSession(run) {
464
+ await this.detachRunningForeground("session switch");
465
+ this.backgroundSessionRuns.delete(run.sessionId);
466
+ this.runtime.engine = run.engine;
467
+ this.activeAbortController = run.abortController;
468
+ this.interruptArmed = false;
469
+ this.foregroundRun = run.promise;
470
+ this.suppressReattachedStreaming.add(run.engine);
471
+ await this.refreshSessionView(systemLine(`reattached running session ${run.sessionId}`));
472
+ this.setBusy(true);
473
+ this.setStatus({ ...this.status, phase: "running", detail: "working" });
474
+ }
373
475
  reduce(event) {
374
476
  this.status = reduceStatus(this.status, event);
375
477
  if (event.type === "usage")
@@ -496,12 +598,17 @@ class WebRepl {
496
598
  if (command.type === "reset") {
497
599
  this.runtime.engine.reset();
498
600
  this.runtime.usage.reset();
499
- this.status = resetStatus(this.runtime);
601
+ this.status = await resetStatus(this.runtime);
500
602
  this.append(systemLine("transcript reset"));
501
603
  return;
502
604
  }
503
605
  if (command.type === "state") {
504
- this.append(systemLine(formatReplData({ ...this.runtime.engine.snapshot(), communicationLog: this.runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
606
+ const contextMetrics = await this.runtime.engine.contextMetrics();
607
+ this.append(systemLine(formatReplData({ ...this.runtime.engine.snapshot(), contextMetrics, communicationLog: this.runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
608
+ return;
609
+ }
610
+ if (command.type === "new") {
611
+ await this.newSession();
505
612
  return;
506
613
  }
507
614
  if (command.type === "sessions") {
@@ -576,20 +683,35 @@ class WebRepl {
576
683
  this.interruptArmed = false;
577
684
  this.setBusy(true);
578
685
  this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
686
+ const engine = this.runtime.engine;
579
687
  try {
580
- for await (const event of this.runtime.engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: promptPayload.displayText })) {
688
+ for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: promptPayload.displayText })) {
689
+ if (this.runtime.engine !== engine)
690
+ continue;
691
+ if (this.suppressReattachedStreaming.has(engine)) {
692
+ if (event.type === "message" || event.type === "terminal" || event.type === "error" || event.type === "context.metrics" || event.type === "usage") {
693
+ if (event.type === "message" || event.type === "terminal" || event.type === "error")
694
+ this.suppressReattachedStreaming.delete(engine);
695
+ this.handleEvent(event);
696
+ }
697
+ continue;
698
+ }
581
699
  this.handleEvent(event);
582
700
  }
583
701
  }
584
702
  catch (error) {
585
- this.finalizeLiveLine(this.assistantLineId);
586
- this.finalizeThinkingLine();
587
- this.finalizeActiveToolLines();
588
- this.assistantLineId = undefined;
589
- this.finalizedThinkingLineId = undefined;
590
- this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
703
+ if (this.runtime.engine === engine) {
704
+ this.finalizeLiveLine(this.assistantLineId);
705
+ this.finalizeThinkingLine();
706
+ this.finalizeActiveToolLines();
707
+ this.assistantLineId = undefined;
708
+ this.finalizedThinkingLineId = undefined;
709
+ this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
710
+ }
591
711
  }
592
712
  finally {
713
+ if (this.runtime.engine !== engine)
714
+ return;
593
715
  if (this.activeAbortController === abortController)
594
716
  this.activeAbortController = undefined;
595
717
  this.interruptArmed = false;
@@ -673,11 +795,13 @@ async function route(req, res, repl) {
673
795
  if (req.method === "POST" && url.pathname === "/api/interrupt")
674
796
  return sendJson(res, repl.interrupt());
675
797
  if (req.method === "GET" && url.pathname === "/api/sessions")
676
- return sendJson(res, { sessions: await repl.listSessions() });
798
+ return sendJson(res, await repl.listSessions());
677
799
  if (req.method === "POST" && url.pathname === "/api/sessions/resume") {
678
800
  const body = await readJsonBody(req);
679
801
  return sendJson(res, await repl.resumeSession(String(body.sessionId ?? "")));
680
802
  }
803
+ if (req.method === "POST" && url.pathname === "/api/sessions/new")
804
+ return sendJson(res, await repl.newSession());
681
805
  if (req.method === "POST" && url.pathname === "/api/sessions/delete") {
682
806
  const body = await readJsonBody(req);
683
807
  return sendJson(res, await repl.deleteSession(String(body.sessionId ?? "")));
@@ -765,8 +889,8 @@ function restoredHistoryLines(runtime) {
765
889
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
766
890
  return { phase: "ready", metrics: { ...metrics, messageCount: runtime.engine.snapshot().messages }, streamedOutputTokens: 0, activityTick: 0 };
767
891
  }
768
- function resetStatus(runtime) {
769
- return initialStatus(runtime, initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount));
892
+ async function resetStatus(runtime) {
893
+ return initialStatus(runtime, await runtime.engine.contextMetrics());
770
894
  }
771
895
  function buildWebPromptPayload(displayText, attachments) {
772
896
  const activeAttachments = attachments.filter((attachment) => displayText.includes(attachment.label));
@@ -1339,26 +1463,190 @@ function formatPlanToolPayload(payload) {
1339
1463
  return sections.filter(Boolean).join("\n");
1340
1464
  }
1341
1465
  function formatToolResult(toolName, output, ok) {
1342
- if (isExecOutput(output)) {
1343
- const status = output.timedOut ? "timed out" : output.exitCode === 0 ? "exit 0" : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
1344
- const sections = [`${status} · ${output.durationMs}ms`, `$ ${output.command}`];
1345
- if (output.stdout)
1346
- sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
1347
- if (output.stderr)
1348
- sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
1349
- if (!output.stdout && !output.stderr)
1350
- sections.push(ok ? "no output" : "no captured output");
1351
- return { text: sections.join("\n"), format: "ansi" };
1352
- }
1466
+ if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
1467
+ return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
1468
+ if (isExecOutput(output))
1469
+ return { text: formatExecToolResult(output, ok), format: "plain", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1470
+ if (toolName === "list" && isRecord(output))
1471
+ return { text: formatListToolResult(output, ok) };
1472
+ if (toolName === "read" && isRecord(output))
1473
+ return { text: formatReadToolResult(output, ok) };
1474
+ if (toolName === "grep" && isRecord(output))
1475
+ return { text: formatGrepToolResult(output, ok) };
1476
+ if (toolName === "search" && isRecord(output))
1477
+ return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1353
1478
  if (toolName === "plan" && isPlanToolPayload(output))
1354
1479
  return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
1355
1480
  if (typeof output === "string")
1356
1481
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1357
1482
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1358
1483
  }
1484
+ function isEditToolOutput(value) {
1485
+ return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
1486
+ }
1487
+ function isEditPatchHunk(value) {
1488
+ return isRecord(value) && typeof value.oldStart === "number" && typeof value.oldLines === "number" && typeof value.newStart === "number" && typeof value.newLines === "number" && Array.isArray(value.lines) && value.lines.every((line) => typeof line === "string");
1489
+ }
1490
+ function formatEditToolDiff(output, ok) {
1491
+ const lines = [
1492
+ `${ok ? output.operation : "failed"} ${output.path}, ${output.replacements} replacement(s)`,
1493
+ `--- ${output.path}`,
1494
+ `+++ ${output.path}`,
1495
+ ];
1496
+ for (const hunk of output.patch) {
1497
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
1498
+ lines.push(...formatEditPatchHunkLines(hunk));
1499
+ }
1500
+ if (output.patch.length === 0)
1501
+ lines.push("no changes");
1502
+ return lines.join("\n");
1503
+ }
1504
+ function formatEditPatchHunkLines(hunk) {
1505
+ const oldLineWidth = diffLineNumberWidth(hunk.oldStart, hunk.oldLines);
1506
+ const newLineWidth = diffLineNumberWidth(hunk.newStart, hunk.newLines);
1507
+ let oldLineNumber = hunk.oldStart;
1508
+ let newLineNumber = hunk.newStart;
1509
+ return hunk.lines.map((rawLine) => {
1510
+ const marker = diffLineMarker(rawLine);
1511
+ if (!marker)
1512
+ return rawLine;
1513
+ const showOldLineNumber = marker !== "+";
1514
+ const showNewLineNumber = marker !== "-";
1515
+ const oldLineLabel = showOldLineNumber ? String(oldLineNumber).padStart(oldLineWidth) : " ".repeat(oldLineWidth);
1516
+ const newLineLabel = showNewLineNumber ? String(newLineNumber).padStart(newLineWidth) : " ".repeat(newLineWidth);
1517
+ if (showOldLineNumber)
1518
+ oldLineNumber += 1;
1519
+ if (showNewLineNumber)
1520
+ newLineNumber += 1;
1521
+ return `${oldLineLabel} ${newLineLabel} │ ${marker}${rawLine.slice(1)}`;
1522
+ });
1523
+ }
1524
+ function diffLineNumberWidth(start, lineCount) {
1525
+ const end = lineCount > 0 ? start + lineCount - 1 : start;
1526
+ return Math.max(String(start).length, String(end).length, 2);
1527
+ }
1528
+ function diffLineMarker(line) {
1529
+ const marker = line[0];
1530
+ return marker === "+" || marker === "-" || marker === " " ? marker : undefined;
1531
+ }
1359
1532
  function isExecOutput(value) {
1360
1533
  return isRecord(value) && typeof value.command === "string" && typeof value.durationMs === "number";
1361
1534
  }
1535
+ function formatExecToolResult(output, ok) {
1536
+ const status = output.timedOut ? "timed out" : output.exitCode === 0 ? "exit 0" : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
1537
+ const lines = ["exec result", `status: ${status}`, `duration: ${output.durationMs}ms`, `command: ${output.command}`];
1538
+ const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
1539
+ const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
1540
+ if (stdout)
1541
+ lines.push("stdout:", stdout);
1542
+ if (stderr)
1543
+ lines.push("stderr:", stderr);
1544
+ if (!stdout && !stderr)
1545
+ lines.push(ok ? "output: (none)" : "output: (not captured)");
1546
+ return lines.join("\n");
1547
+ }
1548
+ function formatListToolResult(output, ok) {
1549
+ const pathValue = typeof output.path === "string" ? output.path : "";
1550
+ const typeValue = typeof output.type === "string" ? output.type : "result";
1551
+ const returnedEntries = typeof output.returnedEntries === "number" ? output.returnedEntries : undefined;
1552
+ const totalFiles = typeof output.totalFiles === "number" ? output.totalFiles : undefined;
1553
+ const totalDirectories = typeof output.totalDirectories === "number" ? output.totalDirectories : undefined;
1554
+ const entries = Array.isArray(output.entries) ? output.entries : [];
1555
+ const names = entries.map((entry) => (isRecord(entry) && typeof entry.name === "string" ? entry.name : undefined)).filter((name) => Boolean(name)).slice(0, 5);
1556
+ const lines = [ok ? "list result" : "failed"];
1557
+ if (pathValue)
1558
+ lines.push(`path: ${pathValue}`);
1559
+ lines.push(`type: ${typeValue}`);
1560
+ const counts = [returnedEntries !== undefined ? `${returnedEntries} shown` : undefined, totalFiles !== undefined ? `${totalFiles} files` : undefined, totalDirectories !== undefined ? `${totalDirectories} dirs` : undefined].filter((value) => Boolean(value));
1561
+ if (counts.length > 0)
1562
+ lines.push(`entries: ${counts.join(" · ")}`);
1563
+ if (names.length > 0)
1564
+ lines.push("sample:", ...names.map((name) => ` ${name}`));
1565
+ return lines.join("\n");
1566
+ }
1567
+ function formatReadToolResult(output, ok) {
1568
+ const error = typeof output.error === "string" ? output.error : undefined;
1569
+ if (!ok || error)
1570
+ return ["failed", error ?? formatReplData(output, 1200)].join("\n");
1571
+ const pathValue = typeof output.path === "string" ? output.path : undefined;
1572
+ const startLine = typeof output.startLine === "number" ? output.startLine : undefined;
1573
+ const endLine = typeof output.endLine === "number" ? output.endLine : undefined;
1574
+ const totalLines = typeof output.totalLines === "number" ? output.totalLines : undefined;
1575
+ const hasMoreBefore = output.hasMoreBefore === true;
1576
+ const hasMoreAfter = output.hasMoreAfter === true;
1577
+ const content = typeof output.content === "string" ? output.content.trimEnd() : "";
1578
+ const lines = ["read result"];
1579
+ if (pathValue)
1580
+ lines.push(`file: ${pathValue}`);
1581
+ if (startLine !== undefined && endLine !== undefined && totalLines !== undefined) {
1582
+ const more = [hasMoreBefore ? "more before" : undefined, hasMoreAfter ? "more after" : undefined].filter((value) => Boolean(value)).join(", ");
1583
+ lines.push(`range: lines ${startLine}-${endLine} of ${totalLines}${more ? ` (${more})` : ""}`);
1584
+ }
1585
+ lines.push("content:", content || "(empty range)");
1586
+ return lines.join("\n");
1587
+ }
1588
+ function formatWebSearchToolResult(output, ok) {
1589
+ const error = typeof output.error === "string" ? output.error : undefined;
1590
+ if (!ok || error)
1591
+ return ["failed", error ?? formatReplData(output, 1200)].join("\n");
1592
+ const provider = typeof output.provider === "string" ? output.provider : "unknown";
1593
+ const query = typeof output.query === "string" ? output.query : "";
1594
+ const returnedResults = typeof output.returnedResults === "number" ? output.returnedResults : undefined;
1595
+ const results = Array.isArray(output.results) ? output.results : [];
1596
+ const lines = [`${returnedResults ?? results.length} web result(s) via ${provider}`];
1597
+ if (query)
1598
+ lines.push(`query: ${query}`);
1599
+ if (output.truncated === true)
1600
+ lines.push("truncated");
1601
+ if (results.length === 0)
1602
+ return [...lines, "no results"].join("\n");
1603
+ results.slice(0, 8).forEach((item, index) => {
1604
+ if (!isRecord(item))
1605
+ return;
1606
+ const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : "Untitled";
1607
+ const url = typeof item.url === "string" ? item.url : "";
1608
+ const published = typeof item.published === "string" ? ` · ${item.published}` : "";
1609
+ lines.push(`[${index + 1}] ${title}${published}`);
1610
+ if (url)
1611
+ lines.push(url);
1612
+ const highlights = Array.isArray(item.highlights) ? item.highlights.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
1613
+ const snippet = highlights[0] ?? (typeof item.text === "string" ? item.text : undefined);
1614
+ if (snippet)
1615
+ lines.push(truncate(snippet.replace(/\s+/gu, " "), 400));
1616
+ });
1617
+ return lines.join("\n");
1618
+ }
1619
+ function formatGrepToolResult(output, ok) {
1620
+ const error = typeof output.error === "string" ? output.error : undefined;
1621
+ if (!ok || error)
1622
+ return ["failed", error ?? formatReplData(output, 1200)].join("\n");
1623
+ const query = typeof output.query === "string" ? output.query : undefined;
1624
+ const grepPath = typeof output.grepPath === "string" ? output.grepPath : typeof output.path === "string" ? output.path : undefined;
1625
+ const returnedMatches = typeof output.returnedMatches === "number" ? output.returnedMatches : undefined;
1626
+ const totalMatchesKnown = typeof output.totalMatchesKnown === "number" ? output.totalMatchesKnown : undefined;
1627
+ const truncated = output.truncated === true;
1628
+ const matches = Array.isArray(output.matches) ? output.matches.filter(isGrepMatchLike) : [];
1629
+ const lines = ["grep result"];
1630
+ if (query !== undefined)
1631
+ lines.push(`query: ${query}`);
1632
+ if (grepPath !== undefined)
1633
+ lines.push(`path: ${grepPath}`);
1634
+ const countParts = [`${returnedMatches ?? matches.length} shown`, totalMatchesKnown !== undefined ? `${totalMatchesKnown} known` : undefined, truncated ? "truncated" : undefined].filter((value) => Boolean(value));
1635
+ lines.push(`matches: ${countParts.join(" · ")}`);
1636
+ if (matches.length === 0)
1637
+ return [...lines, "no matches"].join("\n");
1638
+ lines.push("results:");
1639
+ for (const match of matches)
1640
+ lines.push(formatGrepMatchLine(match));
1641
+ return lines.join("\n");
1642
+ }
1643
+ function isGrepMatchLike(value) {
1644
+ return isRecord(value) && typeof value.file === "string" && typeof value.line === "number" && typeof value.text === "string" && (value.column === undefined || typeof value.column === "number");
1645
+ }
1646
+ function formatGrepMatchLine(match) {
1647
+ const column = match.column !== undefined ? `:${match.column}` : "";
1648
+ return ` ${match.file}:${match.line}${column}: ${match.text}`;
1649
+ }
1362
1650
  function formatReplData(value, maxLength) {
1363
1651
  return truncate(formatReplValue(value), maxLength);
1364
1652
  }
@@ -1456,6 +1744,7 @@ function formatNumber(value) {
1456
1744
  }
1457
1745
  const THINKING_SUMMARY_MAX_LINES = 1000;
1458
1746
  const EXPANDED_SUMMARY_MAX_LINES = 1000;
1747
+ const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
1459
1748
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
1460
1749
  runWebServer().catch((error) => {
1461
1750
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));