@oh-my-pi/pi-coding-agent 15.11.7 → 15.11.8

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/dist/cli.js +363 -356
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/collab/crypto.d.ts +12 -0
  5. package/dist/types/collab/guest.d.ts +21 -0
  6. package/dist/types/collab/host.d.ts +13 -0
  7. package/dist/types/collab/protocol.d.ts +100 -0
  8. package/dist/types/collab/relay-client.d.ts +22 -0
  9. package/dist/types/commands/join.d.ts +12 -0
  10. package/dist/types/config/settings-schema.d.ts +21 -1
  11. package/dist/types/extensibility/slash-commands.d.ts +1 -11
  12. package/dist/types/modes/components/agent-hub.d.ts +13 -0
  13. package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
  14. package/dist/types/modes/components/hook-selector.d.ts +4 -6
  15. package/dist/types/modes/components/segment-track.d.ts +11 -6
  16. package/dist/types/modes/components/status-line/component.d.ts +4 -1
  17. package/dist/types/modes/components/status-line/types.d.ts +9 -0
  18. package/dist/types/modes/interactive-mode.d.ts +7 -0
  19. package/dist/types/modes/types.d.ts +8 -0
  20. package/dist/types/session/agent-session.d.ts +11 -0
  21. package/dist/types/session/session-manager.d.ts +21 -0
  22. package/dist/types/session/snapcompact-inline.d.ts +6 -3
  23. package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
  24. package/package.json +14 -12
  25. package/scripts/bench-guard.ts +71 -0
  26. package/src/cli/args.ts +2 -0
  27. package/src/cli-commands.ts +1 -0
  28. package/src/collab/crypto.ts +57 -0
  29. package/src/collab/guest.ts +421 -0
  30. package/src/collab/host.ts +494 -0
  31. package/src/collab/protocol.ts +191 -0
  32. package/src/collab/relay-client.ts +216 -0
  33. package/src/commands/join.ts +39 -0
  34. package/src/config/model-registry.ts +22 -14
  35. package/src/config/settings-schema.ts +27 -1
  36. package/src/extensibility/slash-commands.ts +1 -97
  37. package/src/internal-urls/docs-index.generated.ts +3 -2
  38. package/src/main.ts +11 -2
  39. package/src/modes/components/agent-hub.ts +119 -22
  40. package/src/modes/components/assistant-message.ts +126 -6
  41. package/src/modes/components/collab-prompt-message.ts +30 -0
  42. package/src/modes/components/hook-selector.ts +4 -5
  43. package/src/modes/components/segment-track.ts +44 -7
  44. package/src/modes/components/status-line/component.ts +21 -1
  45. package/src/modes/components/status-line/presets.ts +1 -1
  46. package/src/modes/components/status-line/segments.ts +13 -0
  47. package/src/modes/components/status-line/types.ts +10 -0
  48. package/src/modes/components/tips.txt +2 -1
  49. package/src/modes/controllers/input-controller.ts +72 -6
  50. package/src/modes/controllers/selector-controller.ts +2 -0
  51. package/src/modes/controllers/streaming-reveal.ts +7 -0
  52. package/src/modes/interactive-mode.ts +12 -4
  53. package/src/modes/types.ts +8 -0
  54. package/src/modes/utils/ui-helpers.ts +7 -0
  55. package/src/sdk.ts +239 -36
  56. package/src/session/agent-session.ts +17 -0
  57. package/src/session/session-manager.ts +44 -0
  58. package/src/session/snapcompact-inline.ts +9 -3
  59. package/src/slash-commands/builtin-registry.ts +210 -0
  60. package/src/tools/read.ts +38 -5
  61. package/src/tools/write.ts +13 -42
@@ -3,8 +3,11 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
5
5
  import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
6
+ import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
6
7
  import { Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
7
8
  import { $ } from "bun";
9
+ import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
10
+ import { CollabHost } from "../collab/host";
8
11
  import type { SettingPath, SettingValue } from "../config/settings";
9
12
  import { settings } from "../config/settings";
10
13
  import {
@@ -42,6 +45,7 @@ import type {
42
45
  SlashCommandResult,
43
46
  SlashCommandRuntime,
44
47
  SlashCommandSpec,
48
+ SubcommandDef,
45
49
  TuiSlashCommandRuntime,
46
50
  } from "./types";
47
51
 
@@ -442,6 +446,115 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
442
446
  runtime.ctx.editor.setText("");
443
447
  },
444
448
  },
449
+ {
450
+ name: "collab",
451
+ description: "Share this session live via a relay",
452
+ inlineHint: "[start|stop|status] [relayUrl]",
453
+ allowArgs: true,
454
+ handleTui: async (command, runtime) => {
455
+ const ctx = runtime.ctx;
456
+ ctx.editor.setText("");
457
+ const args = command.args.trim();
458
+ const [first = ""] = args.split(/\s+/, 1);
459
+ if (first === "stop") {
460
+ if (!ctx.collabHost) {
461
+ ctx.showStatus("Not hosting a collab session");
462
+ return;
463
+ }
464
+ await ctx.collabHost.stop("host stopped");
465
+ ctx.showStatus("Collab stopped");
466
+ return;
467
+ }
468
+ if (first === "status") {
469
+ if (ctx.collabHost) {
470
+ const names = ctx.collabHost.participants.map(p => (p.role === "host" ? `${p.name} (host)` : p.name));
471
+ ctx.showStatus(`Collab: ${names.join(", ")} — ${ctx.collabHost.link}`);
472
+ } else if (ctx.collabGuest) {
473
+ ctx.showStatus("In a collab session as a guest (/leave to exit)");
474
+ } else {
475
+ ctx.showStatus("Not in a collab session");
476
+ }
477
+ return;
478
+ }
479
+ if (ctx.collabGuest) {
480
+ ctx.showError("Already in a collab session as a guest (/leave first)");
481
+ return;
482
+ }
483
+ if (ctx.collabHost) {
484
+ ctx.session.emitNotice("info", `Collab link: ${ctx.collabHost.link}`, "collab");
485
+ return;
486
+ }
487
+ const explicitUrl = first === "start" ? args.slice("start".length).trim() : args;
488
+ const relayInput = explicitUrl || ctx.settings.get("collab.relayUrl") || "";
489
+ if (!relayInput) {
490
+ ctx.showError(
491
+ "No relay configured. Set collab.relayUrl in /settings or pass one: /collab relay.example.com",
492
+ );
493
+ return;
494
+ }
495
+ // Scheme-less relay args default to wss (ws:// must be spelled out for localhost).
496
+ const relayUrl = relayInput.includes("://") ? relayInput : `wss://${relayInput}`;
497
+ const host = new CollabHost(ctx);
498
+ try {
499
+ await host.start(relayUrl);
500
+ } catch (err) {
501
+ ctx.showError(`Failed to start collab session: ${errorMessage(err)}`);
502
+ return;
503
+ }
504
+ ctx.collabHost = host;
505
+ ctx.session.emitNotice(
506
+ "info",
507
+ `Collab link: ${host.link}\nAnyone with this link can read the session and prompt the agent.`,
508
+ "collab",
509
+ );
510
+ },
511
+ },
512
+ {
513
+ name: "join",
514
+ description: "Join a shared collab session",
515
+ inlineHint: "<link>",
516
+ allowArgs: true,
517
+ handleTui: async (command, runtime) => {
518
+ const ctx = runtime.ctx;
519
+ ctx.editor.setText("");
520
+ const link = command.args.trim();
521
+ if (!link) {
522
+ ctx.showError("Usage: /join <link>");
523
+ return;
524
+ }
525
+ if (ctx.collabHost) {
526
+ ctx.showError("Stop hosting first (/collab stop)");
527
+ return;
528
+ }
529
+ if (ctx.collabGuest) {
530
+ ctx.showError("Already in a collab session (/leave first)");
531
+ return;
532
+ }
533
+ try {
534
+ await new CollabGuestLink(ctx).join(link);
535
+ } catch (err) {
536
+ ctx.showError(`Failed to join collab session: ${errorMessage(err)}`);
537
+ }
538
+ },
539
+ },
540
+ {
541
+ name: "leave",
542
+ description: "Leave the collab session",
543
+ handleTui: async (_command, runtime) => {
544
+ const ctx = runtime.ctx;
545
+ ctx.editor.setText("");
546
+ if (ctx.collabGuest) {
547
+ await ctx.collabGuest.leave("left");
548
+ return;
549
+ }
550
+ if (ctx.collabHost) {
551
+ await ctx.collabHost.stop("host stopped");
552
+ ctx.showStatus("Collab stopped");
553
+ return;
554
+ }
555
+ ctx.showStatus("Not in a collab session");
556
+ },
557
+ },
445
558
  {
446
559
  name: "browser",
447
560
  description: "Toggle browser headless vs visible mode",
@@ -1853,6 +1966,70 @@ for (const command of BUILTIN_SLASH_COMMAND_REGISTRY) {
1853
1966
 
1854
1967
  export const BUILTIN_SLASH_COMMAND_RESERVED_NAMES: ReadonlySet<string> = new Set(BUILTIN_SLASH_COMMAND_LOOKUP.keys());
1855
1968
 
1969
+ /**
1970
+ * Build getArgumentCompletions from declarative subcommand definitions.
1971
+ * Returns subcommand names filtered by prefix in the dropdown.
1972
+ */
1973
+ function buildArgumentCompletions(subcommands: SubcommandDef[]): (prefix: string) => AutocompleteItem[] | null {
1974
+ return (argumentPrefix: string) => {
1975
+ if (argumentPrefix.includes(" ")) return null; // past the subcommand
1976
+ const lower = argumentPrefix.toLowerCase();
1977
+ const matches = subcommands
1978
+ .filter(s => s.name.startsWith(lower))
1979
+ .map(s => ({
1980
+ value: `${s.name} `,
1981
+ label: s.name,
1982
+ description: s.description,
1983
+ hint: s.usage,
1984
+ }));
1985
+ return matches.length > 0 ? matches : null;
1986
+ };
1987
+ }
1988
+
1989
+ /**
1990
+ * Build getInlineHint from declarative subcommand definitions.
1991
+ * Shows remaining completion + usage as dim ghost text after cursor.
1992
+ */
1993
+ function buildSubcommandInlineHint(subcommands: SubcommandDef[]): (argumentText: string) => string | null {
1994
+ return (argumentText: string) => {
1995
+ const trimmed = argumentText.trimStart();
1996
+ const spaceIndex = trimmed.indexOf(" ");
1997
+
1998
+ if (spaceIndex === -1) {
1999
+ // Still typing subcommand name — show remaining chars + usage
2000
+ const prefix = trimmed.toLowerCase();
2001
+ if (prefix.length === 0) return null;
2002
+ const match = subcommands.find(s => s.name.startsWith(prefix));
2003
+ if (!match) return null;
2004
+ const remaining = match.name.slice(prefix.length);
2005
+ return remaining + (match.usage ? ` ${match.usage}` : "");
2006
+ }
2007
+
2008
+ // Subcommand typed — show remaining usage params
2009
+ const subName = trimmed.slice(0, spaceIndex).toLowerCase();
2010
+ const afterSub = trimmed.slice(spaceIndex + 1);
2011
+ const sub = subcommands.find(s => s.name === subName);
2012
+ if (!sub?.usage) return null;
2013
+
2014
+ if (afterSub.length > 0) {
2015
+ const usageParts = sub.usage.split(" ");
2016
+ const inputParts = afterSub.trim().split(/\s+/);
2017
+ const remaining = usageParts.slice(inputParts.length);
2018
+ return remaining.length > 0 ? remaining.join(" ") : null;
2019
+ }
2020
+
2021
+ return sub.usage;
2022
+ };
2023
+ }
2024
+
2025
+ /**
2026
+ * Build getInlineHint for commands with a simple static hint string.
2027
+ * Shows the hint only when no arguments have been typed yet.
2028
+ */
2029
+ function buildStaticInlineHint(hint: string): (argumentText: string) => string | null {
2030
+ return (argumentText: string) => (argumentText.trim().length === 0 ? hint : null);
2031
+ }
2032
+
1856
2033
  /** Builtin command metadata used for slash-command autocomplete and help text. */
1857
2034
  export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_REGISTRY.map(
1858
2035
  command => ({
@@ -1864,6 +2041,32 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
1864
2041
  }),
1865
2042
  );
1866
2043
 
2044
+ /**
2045
+ * Materialized builtin slash commands with completion functions derived from
2046
+ * declarative subcommand/hint definitions.
2047
+ */
2048
+ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
2049
+ BuiltinSlashCommand & {
2050
+ getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
2051
+ getInlineHint?: (argumentText: string) => string | null;
2052
+ }
2053
+ > = BUILTIN_SLASH_COMMAND_DEFS.map(cmd => {
2054
+ if (cmd.subcommands) {
2055
+ return {
2056
+ ...cmd,
2057
+ getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
2058
+ getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
2059
+ };
2060
+ }
2061
+ if (cmd.inlineHint) {
2062
+ return {
2063
+ ...cmd,
2064
+ getInlineHint: buildStaticInlineHint(cmd.inlineHint),
2065
+ };
2066
+ }
2067
+ return cmd;
2068
+ });
2069
+
1867
2070
  /**
1868
2071
  * Unified registry exposed for cross-mode tooling. Each spec carries at least
1869
2072
  * one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
@@ -1890,6 +2093,13 @@ export async function executeBuiltinSlashCommand(
1890
2093
  if (parsed.args.length > 0 && !command.allowArgs) {
1891
2094
  return false;
1892
2095
  }
2096
+ // Collab guests run a read-mostly replica: session-mutating builtins are
2097
+ // host-only; the allowlist covers purely local/read-only commands.
2098
+ if (runtime.ctx.collabGuest && !COLLAB_GUEST_ALLOWED_COMMANDS[command.name]) {
2099
+ runtime.ctx.showStatus(`/${command.name} is host-only during a collab session`);
2100
+ runtime.ctx.editor.setText("");
2101
+ return true;
2102
+ }
1893
2103
  if (command.handleTui) {
1894
2104
  const result = await command.handleTui(parsed, runtime);
1895
2105
  if (result && typeof result === "object" && "prompt" in result) return result.prompt;
package/src/tools/read.ts CHANGED
@@ -8,6 +8,7 @@ import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
8
8
  import type { Component } from "@oh-my-pi/pi-tui";
9
9
  import { Text } from "@oh-my-pi/pi-tui";
10
10
  import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
11
+ import { LRUCache } from "lru-cache/raw";
11
12
  import * as z from "zod/v4";
12
13
  import {
13
14
  canonicalSnapshotKey,
@@ -100,6 +101,28 @@ import {
100
101
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
101
102
  import { toolResult } from "./tool-result";
102
103
 
104
+ // Per-session memo for tree-sitter summaries. `summarizeCode` is a pure function
105
+ // of (code, path, fold settings) but costs ~12-18ms for a ~1500-line file, and a
106
+ // repeat summary read of the same unchanged file re-parses from scratch. Key on
107
+ // the content hash of the freshly-read bytes (+ path + fold settings): the file
108
+ // is still read fresh on every call, so a hit only reuses the deterministic
109
+ // parse — there is no staleness window and no stat guard is needed. Bounded LRU,
110
+ // aged out with the session via WeakMap.
111
+ // Unusable results (not parsed, or nothing elided) are memoized as `false`: the
112
+ // full SummaryResult embeds the whole source in kept segments, and the caller
113
+ // only ever renders `parsed && elided` summaries — caching the segments would
114
+ // retain up to 48 near-2MiB sources just to remember "no summary".
115
+ const SUMMARY_CACHE_MAX = 48;
116
+ const summaryParseCaches = new WeakMap<object, LRUCache<string, SummaryResult | false>>();
117
+ function getSummaryParseCache(session: object): LRUCache<string, SummaryResult | false> {
118
+ let cache = summaryParseCaches.get(session);
119
+ if (!cache) {
120
+ cache = new LRUCache<string, SummaryResult | false>({ max: SUMMARY_CACHE_MAX });
121
+ summaryParseCaches.set(session, cache);
122
+ }
123
+ return cache;
124
+ }
125
+
103
126
  // Document types converted to markdown via markit.
104
127
  const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
105
128
 
@@ -1614,15 +1637,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1614
1637
  if (lineCount > MAX_SUMMARY_LINES) return null;
1615
1638
  if (lineCount < this.session.settings.get("read.summarize.minTotalLines")) return null;
1616
1639
 
1640
+ const minBodyLines = this.session.settings.get("read.summarize.minBodyLines");
1641
+ const minCommentLines = this.session.settings.get("read.summarize.minCommentLines");
1642
+ const unfoldUntilLines = this.session.settings.get("read.summarize.unfoldUntil");
1643
+ const unfoldLimitLines = this.session.settings.get("read.summarize.unfoldLimit");
1644
+ const cache = getSummaryParseCache(this.session);
1645
+ const cacheKey = `${absolutePath}\0${Bun.hash(code)}\0${minBodyLines},${minCommentLines},${unfoldUntilLines},${unfoldLimitLines}`;
1646
+ const memoized = cache.get(cacheKey);
1647
+ if (memoized !== undefined) return memoized || null;
1617
1648
  const result = summarizeCode({
1618
1649
  code,
1619
1650
  path: absolutePath,
1620
- minBodyLines: this.session.settings.get("read.summarize.minBodyLines"),
1621
- minCommentLines: this.session.settings.get("read.summarize.minCommentLines"),
1622
- unfoldUntilLines: this.session.settings.get("read.summarize.unfoldUntil"),
1623
- unfoldLimitLines: this.session.settings.get("read.summarize.unfoldLimit"),
1651
+ minBodyLines,
1652
+ minCommentLines,
1653
+ unfoldUntilLines,
1654
+ unfoldLimitLines,
1624
1655
  });
1625
- return result;
1656
+ const usable = result.parsed && result.elided ? result : false;
1657
+ cache.set(cacheKey, usable);
1658
+ return usable || null;
1626
1659
  } catch {
1627
1660
  return null;
1628
1661
  }
@@ -583,9 +583,10 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
583
583
 
584
584
  /**
585
585
  * Resolve a single `conflict://<N>` write by splicing the recorded
586
- * marker region in the registered file with `replacementContent`,
587
- * then routing the new file content through the normal writethrough
588
- * pipeline so LSP format/diagnostics still run.
586
+ * marker region in the registered file with `replacementContent`.
587
+ * The write deliberately bypasses the LSP writethrough: the file may
588
+ * still hold other unresolved marker blocks, so formatting could
589
+ * corrupt them and diagnostics would be marker-noise anyway.
589
590
  *
590
591
  * Entry ids are session-stable: they keep working even after later
591
592
  * writes resolve other blocks in the same file. The recorded range
@@ -597,7 +598,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
597
598
  replacementContent: string,
598
599
  stripped: boolean,
599
600
  signal: AbortSignal | undefined,
600
- context: AgentToolContext | undefined,
601
601
  ): Promise<AgentToolResult<WriteToolDetails>> {
602
602
  const absolutePath = entry.absolutePath;
603
603
  if (!(await fs.exists(absolutePath))) {
@@ -608,8 +608,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
608
608
  const originalText = await Bun.file(absolutePath).text();
609
609
  const newContent = spliceConflict(originalText, entry, expanded);
610
610
 
611
- const batchRequest = getLspBatchRequest(context?.toolCall);
612
- const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
611
+ await writethroughNoop(absolutePath, newContent, signal);
613
612
  invalidateFsScanAfterWrite(absolutePath);
614
613
  this.session.bumpFileMutationVersion?.(absolutePath);
615
614
  this.session.fileSnapshotStore?.invalidate(absolutePath);
@@ -643,21 +642,9 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
643
642
  resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
644
643
  }
645
644
 
646
- if (!diagnostics) {
647
- return {
648
- content: [{ type: "text", text: resultText }],
649
- details: { resolvedPath: absolutePath },
650
- };
651
- }
652
645
  return {
653
646
  content: [{ type: "text", text: resultText }],
654
- details: {
655
- resolvedPath: absolutePath,
656
- diagnostics,
657
- meta: outputMeta()
658
- .diagnostics(diagnostics.summary, diagnostics.messages ?? [])
659
- .get(),
660
- },
647
+ details: { resolvedPath: absolutePath },
661
648
  };
662
649
  }
663
650
 
@@ -670,7 +657,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
670
657
  replacementContent: string,
671
658
  stripped: boolean,
672
659
  signal: AbortSignal | undefined,
673
- context: AgentToolContext | undefined,
674
660
  ): Promise<AgentToolResult<WriteToolDetails>> {
675
661
  const entry = getConflictHistory(this.session).get(id);
676
662
  if (!entry) {
@@ -678,7 +664,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
678
664
  `Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`,
679
665
  );
680
666
  }
681
- return this.#resolveConflict(entry, replacementContent, stripped, signal, context);
667
+ return this.#resolveConflict(entry, replacementContent, stripped, signal);
682
668
  }
683
669
 
684
670
  /**
@@ -700,7 +686,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
700
686
  replacementContent: string,
701
687
  stripped: boolean,
702
688
  signal: AbortSignal | undefined,
703
- context: AgentToolContext | undefined,
704
689
  ): Promise<AgentToolResult<WriteToolDetails>> {
705
690
  const history = getConflictHistory(this.session);
706
691
  const allEntries = history.entries();
@@ -717,8 +702,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
717
702
  byFile.set(entry.absolutePath, bucket);
718
703
  }
719
704
 
720
- const batchRequest = getLspBatchRequest(context?.toolCall);
721
- const allDiagnostics: FileDiagnosticsResult[] = [];
722
705
  const succeededFiles: { displayPath: string; count: number; header?: string }[] = [];
723
706
  const failedFiles: { displayPath: string; count: number; error: string }[] = [];
724
707
  let totalResolvedIds = 0;
@@ -776,7 +759,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
776
759
  continue;
777
760
  }
778
761
 
779
- const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
762
+ await writethroughNoop(absolutePath, text, signal);
780
763
  invalidateFsScanAfterWrite(absolutePath);
781
764
  this.session.bumpFileMutationVersion?.(absolutePath);
782
765
  this.session.fileSnapshotStore?.invalidate(absolutePath);
@@ -785,7 +768,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
785
768
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, text);
786
769
  succeededFiles.push({ displayPath: sample.displayPath, count: resolvedEntries.length, header });
787
770
  totalResolvedIds += resolvedEntries.length;
788
- if (diagnostics) allDiagnostics.push(diagnostics);
789
771
  }
790
772
 
791
773
  const summaryLines: string[] = [];
@@ -819,23 +801,12 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
819
801
  }
820
802
  const resultText = summaryLines.join("\n");
821
803
 
822
- if (allDiagnostics.length === 0) {
823
- if (failedFiles.length > 0 && succeededFiles.length === 0) {
824
- throw new ToolError(resultText);
825
- }
826
- return {
827
- content: [{ type: "text", text: resultText }],
828
- details: {},
829
- isError: failedFiles.length > 0 ? true : undefined,
830
- };
804
+ if (failedFiles.length > 0 && succeededFiles.length === 0) {
805
+ throw new ToolError(resultText);
831
806
  }
832
- const mergedSummary = allDiagnostics.map(d => d.summary).join("\n");
833
- const mergedMessages = allDiagnostics.flatMap(d => d.messages ?? []);
834
807
  return {
835
808
  content: [{ type: "text", text: resultText }],
836
- details: {
837
- meta: outputMeta().diagnostics(mergedSummary, mergedMessages).get(),
838
- },
809
+ details: {},
839
810
  isError: failedFiles.length > 0 ? true : undefined,
840
811
  };
841
812
  }
@@ -885,8 +856,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
885
856
  }
886
857
  const result =
887
858
  conflictUri.id === "*"
888
- ? await this.#resolveAllConflicts(cleanContent, stripped, signal, context)
889
- : await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal, context);
859
+ ? await this.#resolveAllConflicts(cleanContent, stripped, signal)
860
+ : await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal);
890
861
  if (conflictUri.recoveredPrefix !== undefined) {
891
862
  appendNoteToResult(
892
863
  result,