@dreb/coding-agent 2.30.0 → 2.31.0

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 (37) hide show
  1. package/README.md +12 -1
  2. package/dist/core/agent-session.d.ts +16 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +58 -2
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/nested-context.d.ts +60 -0
  7. package/dist/core/nested-context.d.ts.map +1 -0
  8. package/dist/core/nested-context.js +276 -0
  9. package/dist/core/nested-context.js.map +1 -0
  10. package/dist/core/resource-loader.d.ts +5 -0
  11. package/dist/core/resource-loader.d.ts.map +1 -1
  12. package/dist/core/resource-loader.js +12 -12
  13. package/dist/core/resource-loader.js.map +1 -1
  14. package/dist/core/settings-manager.d.ts +10 -0
  15. package/dist/core/settings-manager.d.ts.map +1 -1
  16. package/dist/core/settings-manager.js +11 -0
  17. package/dist/core/settings-manager.js.map +1 -1
  18. package/dist/modes/interactive/components/copy-selector.d.ts +3 -3
  19. package/dist/modes/interactive/components/copy-selector.d.ts.map +1 -1
  20. package/dist/modes/interactive/components/copy-selector.js +4 -5
  21. package/dist/modes/interactive/components/copy-selector.js.map +1 -1
  22. package/dist/modes/interactive/components/settings-selector.d.ts +3 -0
  23. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  24. package/dist/modes/interactive/components/settings-selector.js +10 -0
  25. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/dist/modes/interactive/interactive-mode.js +29 -13
  28. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/dist/utils/clipboard.d.ts.map +1 -1
  30. package/dist/utils/clipboard.js +47 -10
  31. package/dist/utils/clipboard.js.map +1 -1
  32. package/dist/utils/message-text.d.ts +12 -0
  33. package/dist/utils/message-text.d.ts.map +1 -1
  34. package/dist/utils/message-text.js +31 -19
  35. package/dist/utils/message-text.js.map +1 -1
  36. package/docs/settings.md +18 -0
  37. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;CAAE,CAAC;AAE1E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAyE5E","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw fall through to platform-specific tools */\n\t}\n\n\t// Also try native tools (best effort for local sessions)\n\tconst p = platform();\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;CAAE,CAAC;AA2B1E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwF5E","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\n/**\n * Best-effort write to the native clipboard module. Returns true on success.\n *\n * On Linux this is the source of issue 286: the native module (clipboard-rs)\n * uses an X11 backend whose in-process selection-serving thread calls\n * `println!(\"Somebody else owns the clipboard now\")` (clipboard-rs\n * src/platform/x11.rs) on `SelectionClear` — i.e. whenever another app takes\n * the clipboard. That write goes to the real stdout file descriptor from native\n * code, so no JS-level stdout/stderr guard can intercept it; it lands in the TUI\n * input region. Callers therefore use this only where it is safe (macOS/Windows,\n * which have no serving thread) or as a Linux last resort when no CLI clipboard\n * tool exists.\n */\nasync function tryNativeClipboard(text: string): Promise<boolean> {\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn true;\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw — caller falls through to other methods */\n\t}\n\treturn false;\n}\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\tconst p = platform();\n\n\t// On macOS/Windows the native module talks to OS clipboard APIs directly and\n\t// does not spawn a background selection-serving thread, so prefer it there.\n\t// On Linux we intentionally do NOT try the native module first — see\n\t// tryNativeClipboard for why (issue 286) — and prefer controlled-stdio\n\t// subprocess tools instead, falling back to native only as a last resort.\n\tif (p !== \"linux\") {\n\t\tif (await tryNativeClipboard(text)) {\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Prefer controlled-stdio subprocess tools (Termux, Wayland,\n\t\t\t// X11). Each owns the selection in its own process with stdout/stderr\n\t\t\t// redirected to /dev/null, so its clipboard-ownership chatter cannot\n\t\t\t// leak into the TUI — unlike the in-process native module (issue 286).\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead.\n\t\t\t\t\t// detached: true puts wl-copy in its own session (setsid) so it keeps\n\t\t\t\t\t// serving the clipboard after we exit and has no controlling terminal.\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"], detached: true });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\n\t\t\t// Linux last resort: the native module. Only reached when no CLI\n\t\t\t// clipboard tool is available. This can reintroduce the issue-286\n\t\t\t// stdout leak on X11 ownership changes, but a working clipboard beats\n\t\t\t// none, and OSC 52 was already emitted above regardless.\n\t\t\tif (await tryNativeClipboard(text)) {\n\t\t\t\treturn { method: \"native\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
@@ -11,21 +11,46 @@ function copyToX11Clipboard(options) {
11
11
  execSync("xsel --clipboard --input", options);
12
12
  }
13
13
  }
14
- export async function copyToClipboard(text) {
15
- // Always emit OSC 52 - works over SSH/mosh, harmless locally
16
- const encoded = Buffer.from(text).toString("base64");
17
- process.stdout.write(`\x1b]52;c;${encoded}\x07`);
14
+ /**
15
+ * Best-effort write to the native clipboard module. Returns true on success.
16
+ *
17
+ * On Linux this is the source of issue 286: the native module (clipboard-rs)
18
+ * uses an X11 backend whose in-process selection-serving thread calls
19
+ * `println!("Somebody else owns the clipboard now")` (clipboard-rs
20
+ * src/platform/x11.rs) on `SelectionClear` — i.e. whenever another app takes
21
+ * the clipboard. That write goes to the real stdout file descriptor from native
22
+ * code, so no JS-level stdout/stderr guard can intercept it; it lands in the TUI
23
+ * input region. Callers therefore use this only where it is safe (macOS/Windows,
24
+ * which have no serving thread) or as a Linux last resort when no CLI clipboard
25
+ * tool exists.
26
+ */
27
+ async function tryNativeClipboard(text) {
18
28
  try {
19
29
  if (clipboard) {
20
30
  await clipboard.setText(text);
21
- return { method: "native" };
31
+ return true;
22
32
  }
23
33
  }
24
34
  catch {
25
- /* Native clipboard module threw — fall through to platform-specific tools */
35
+ /* Native clipboard module threw — caller falls through to other methods */
26
36
  }
27
- // Also try native tools (best effort for local sessions)
37
+ return false;
38
+ }
39
+ export async function copyToClipboard(text) {
40
+ // Always emit OSC 52 - works over SSH/mosh, harmless locally
41
+ const encoded = Buffer.from(text).toString("base64");
42
+ process.stdout.write(`\x1b]52;c;${encoded}\x07`);
28
43
  const p = platform();
44
+ // On macOS/Windows the native module talks to OS clipboard APIs directly and
45
+ // does not spawn a background selection-serving thread, so prefer it there.
46
+ // On Linux we intentionally do NOT try the native module first — see
47
+ // tryNativeClipboard for why (issue 286) — and prefer controlled-stdio
48
+ // subprocess tools instead, falling back to native only as a last resort.
49
+ if (p !== "linux") {
50
+ if (await tryNativeClipboard(text)) {
51
+ return { method: "native" };
52
+ }
53
+ }
29
54
  const options = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
30
55
  try {
31
56
  if (p === "darwin") {
@@ -37,7 +62,10 @@ export async function copyToClipboard(text) {
37
62
  return { method: "platform" };
38
63
  }
39
64
  else {
40
- // Linux. Try Termux, Wayland, or X11 clipboard tools.
65
+ // Linux. Prefer controlled-stdio subprocess tools (Termux, Wayland,
66
+ // X11). Each owns the selection in its own process with stdout/stderr
67
+ // redirected to /dev/null, so its clipboard-ownership chatter cannot
68
+ // leak into the TUI — unlike the in-process native module (issue 286).
41
69
  if (process.env.TERMUX_VERSION) {
42
70
  try {
43
71
  execSync("termux-clipboard-set", options);
@@ -54,8 +82,10 @@ export async function copyToClipboard(text) {
54
82
  try {
55
83
  // Verify wl-copy exists (spawn errors are async and won't be caught)
56
84
  execSync("which wl-copy", { stdio: "ignore" });
57
- // wl-copy with execSync hangs due to fork behavior; use spawn instead
58
- const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] });
85
+ // wl-copy with execSync hangs due to fork behavior; use spawn instead.
86
+ // detached: true puts wl-copy in its own session (setsid) so it keeps
87
+ // serving the clipboard after we exit and has no controlling terminal.
88
+ const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"], detached: true });
59
89
  proc.on("error", () => {
60
90
  // Spawn failed after which check (TOCTOU, permissions, etc.)
61
91
  });
@@ -80,6 +110,13 @@ export async function copyToClipboard(text) {
80
110
  copyToX11Clipboard(options);
81
111
  return { method: "platform" };
82
112
  }
113
+ // Linux last resort: the native module. Only reached when no CLI
114
+ // clipboard tool is available. This can reintroduce the issue-286
115
+ // stdout leak on X11 ownership changes, but a working clipboard beats
116
+ // none, and OSC 52 was already emitted above regardless.
117
+ if (await tryNativeClipboard(text)) {
118
+ return { method: "native" };
119
+ }
83
120
  }
84
121
  }
85
122
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw fall through to platform-specific tools */\n\t}\n\n\t// Also try native tools (best effort for local sessions)\n\tconst p = platform();\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\n/**\n * Best-effort write to the native clipboard module. Returns true on success.\n *\n * On Linux this is the source of issue 286: the native module (clipboard-rs)\n * uses an X11 backend whose in-process selection-serving thread calls\n * `println!(\"Somebody else owns the clipboard now\")` (clipboard-rs\n * src/platform/x11.rs) on `SelectionClear` — i.e. whenever another app takes\n * the clipboard. That write goes to the real stdout file descriptor from native\n * code, so no JS-level stdout/stderr guard can intercept it; it lands in the TUI\n * input region. Callers therefore use this only where it is safe (macOS/Windows,\n * which have no serving thread) or as a Linux last resort when no CLI clipboard\n * tool exists.\n */\nasync function tryNativeClipboard(text: string): Promise<boolean> {\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn true;\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw — caller falls through to other methods */\n\t}\n\treturn false;\n}\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\tconst p = platform();\n\n\t// On macOS/Windows the native module talks to OS clipboard APIs directly and\n\t// does not spawn a background selection-serving thread, so prefer it there.\n\t// On Linux we intentionally do NOT try the native module first — see\n\t// tryNativeClipboard for why (issue 286) — and prefer controlled-stdio\n\t// subprocess tools instead, falling back to native only as a last resort.\n\tif (p !== \"linux\") {\n\t\tif (await tryNativeClipboard(text)) {\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Prefer controlled-stdio subprocess tools (Termux, Wayland,\n\t\t\t// X11). Each owns the selection in its own process with stdout/stderr\n\t\t\t// redirected to /dev/null, so its clipboard-ownership chatter cannot\n\t\t\t// leak into the TUI — unlike the in-process native module (issue 286).\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead.\n\t\t\t\t\t// detached: true puts wl-copy in its own session (setsid) so it keeps\n\t\t\t\t\t// serving the clipboard after we exit and has no controlling terminal.\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"], detached: true });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\n\t\t\t// Linux last resort: the native module. Only reached when no CLI\n\t\t\t// clipboard tool is available. This can reintroduce the issue-286\n\t\t\t// stdout leak on X11 ownership changes, but a working clipboard beats\n\t\t\t// none, and OSC 52 was already emitted above regardless.\n\t\t\tif (await tryNativeClipboard(text)) {\n\t\t\t\treturn { method: \"native\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
@@ -13,6 +13,18 @@ export declare function extractCopyableText(message: AgentMessage): string;
13
13
  * Get a short label for a message's role (used in the copy selector UI).
14
14
  */
15
15
  export declare function getMessageRoleLabel(message: AgentMessage): string;
16
+ /**
17
+ * Extract the thinking/reasoning text from a message, if any.
18
+ * Only assistant messages carry thinking blocks. Returns a single labeled block
19
+ * (`[thinking]\n<reasoning>`) suitable for copying, or an empty string when there
20
+ * is no thinking content (non-assistant role, or assistant with no thinking blocks).
21
+ */
22
+ export declare function extractThinkingText(message: AgentMessage): string;
23
+ /**
24
+ * Normalize text to a single-line preview: collapse whitespace, trim, truncate to ~200 chars.
25
+ * Caller handles further width truncation for display. Returns "[no text content]" for empty input.
26
+ */
27
+ export declare function toSingleLinePreview(text: string): string;
16
28
  /**
17
29
  * Get a single-line preview of a message's content (for display in selector).
18
30
  * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.
@@ -1 +1 @@
1
- {"version":3,"file":"message-text.d.ts","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AASrD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAW/D","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\tconst text = extractCopyableText(message);\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\tconst parts: string[] = [];\n\n\t// Collect text blocks\n\tconst textParts = message.content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text);\n\n\tif (textParts.length > 0) {\n\t\tparts.push(textParts.join(\"\\n\"));\n\t}\n\n\t// Collect thinking blocks with header\n\tfor (const block of message.content) {\n\t\tif (block.type === \"thinking\" && \"thinking\" in block) {\n\t\t\tparts.push(`[thinking]\\n${block.thinking}`);\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
1
+ {"version":3,"file":"message-text.d.ts","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AASrD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAcjE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAE/D","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Extract the thinking/reasoning text from a message, if any.\n * Only assistant messages carry thinking blocks. Returns a single labeled block\n * (`[thinking]\\n<reasoning>`) suitable for copying, or an empty string when there\n * is no thinking content (non-assistant role, or assistant with no thinking blocks).\n */\nexport function extractThinkingText(message: AgentMessage): string {\n\tif (message.role !== \"assistant\") {\n\t\treturn \"\";\n\t}\n\tconst thinkingParts = (message as AssistantMessage).content\n\t\t.filter(\n\t\t\t(block): block is { type: \"thinking\"; thinking: string } => block.type === \"thinking\" && \"thinking\" in block,\n\t\t)\n\t\t.map((block) => block.thinking);\n\n\tif (thinkingParts.length === 0) {\n\t\treturn \"\";\n\t}\n\treturn `[thinking]\\n${thinkingParts.join(\"\\n\\n\")}`;\n}\n\n/**\n * Normalize text to a single-line preview: collapse whitespace, trim, truncate to ~200 chars.\n * Caller handles further width truncation for display. Returns \"[no text content]\" for empty input.\n */\nexport function toSingleLinePreview(text: string): string {\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\treturn toSingleLinePreview(extractCopyableText(message));\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\t// Only visible text blocks. Thinking is excluded by default and surfaced\n\t// separately via extractThinkingText (see issue 285).\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
@@ -51,11 +51,28 @@ export function getMessageRoleLabel(message) {
51
51
  }
52
52
  }
53
53
  /**
54
- * Get a single-line preview of a message's content (for display in selector).
55
- * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.
54
+ * Extract the thinking/reasoning text from a message, if any.
55
+ * Only assistant messages carry thinking blocks. Returns a single labeled block
56
+ * (`[thinking]\n<reasoning>`) suitable for copying, or an empty string when there
57
+ * is no thinking content (non-assistant role, or assistant with no thinking blocks).
56
58
  */
57
- export function getMessagePreview(message) {
58
- const text = extractCopyableText(message);
59
+ export function extractThinkingText(message) {
60
+ if (message.role !== "assistant") {
61
+ return "";
62
+ }
63
+ const thinkingParts = message.content
64
+ .filter((block) => block.type === "thinking" && "thinking" in block)
65
+ .map((block) => block.thinking);
66
+ if (thinkingParts.length === 0) {
67
+ return "";
68
+ }
69
+ return `[thinking]\n${thinkingParts.join("\n\n")}`;
70
+ }
71
+ /**
72
+ * Normalize text to a single-line preview: collapse whitespace, trim, truncate to ~200 chars.
73
+ * Caller handles further width truncation for display. Returns "[no text content]" for empty input.
74
+ */
75
+ export function toSingleLinePreview(text) {
59
76
  if (!text) {
60
77
  return "[no text content]";
61
78
  }
@@ -66,6 +83,13 @@ export function getMessagePreview(message) {
66
83
  }
67
84
  return singleLine.slice(0, 200);
68
85
  }
86
+ /**
87
+ * Get a single-line preview of a message's content (for display in selector).
88
+ * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.
89
+ */
90
+ export function getMessagePreview(message) {
91
+ return toSingleLinePreview(extractCopyableText(message));
92
+ }
69
93
  // --- Internal helpers ---
70
94
  function extractTextBlocks(content) {
71
95
  return content
@@ -80,21 +104,9 @@ function extractUserText(message) {
80
104
  return extractTextBlocks(message.content);
81
105
  }
82
106
  function extractAssistantText(message) {
83
- const parts = [];
84
- // Collect text blocks
85
- const textParts = message.content
86
- .filter((block) => block.type === "text")
87
- .map((block) => block.text);
88
- if (textParts.length > 0) {
89
- parts.push(textParts.join("\n"));
90
- }
91
- // Collect thinking blocks with header
92
- for (const block of message.content) {
93
- if (block.type === "thinking" && "thinking" in block) {
94
- parts.push(`[thinking]\n${block.thinking}`);
95
- }
96
- }
97
- return parts.join("\n");
107
+ // Only visible text blocks. Thinking is excluded by default and surfaced
108
+ // separately via extractThinkingText (see issue 285).
109
+ return extractTextBlocks(message.content);
98
110
  }
99
111
  function extractToolResultText(message) {
100
112
  const textContent = extractTextBlocks(message.content);
@@ -1 +1 @@
1
- {"version":3,"file":"message-text.js","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\tconst text = extractCopyableText(message);\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\tconst parts: string[] = [];\n\n\t// Collect text blocks\n\tconst textParts = message.content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text);\n\n\tif (textParts.length > 0) {\n\t\tparts.push(textParts.join(\"\\n\"));\n\t}\n\n\t// Collect thinking blocks with header\n\tfor (const block of message.content) {\n\t\tif (block.type === \"thinking\" && \"thinking\" in block) {\n\t\t\tparts.push(`[thinking]\\n${block.thinking}`);\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
1
+ {"version":3,"file":"message-text.js","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Extract the thinking/reasoning text from a message, if any.\n * Only assistant messages carry thinking blocks. Returns a single labeled block\n * (`[thinking]\\n<reasoning>`) suitable for copying, or an empty string when there\n * is no thinking content (non-assistant role, or assistant with no thinking blocks).\n */\nexport function extractThinkingText(message: AgentMessage): string {\n\tif (message.role !== \"assistant\") {\n\t\treturn \"\";\n\t}\n\tconst thinkingParts = (message as AssistantMessage).content\n\t\t.filter(\n\t\t\t(block): block is { type: \"thinking\"; thinking: string } => block.type === \"thinking\" && \"thinking\" in block,\n\t\t)\n\t\t.map((block) => block.thinking);\n\n\tif (thinkingParts.length === 0) {\n\t\treturn \"\";\n\t}\n\treturn `[thinking]\\n${thinkingParts.join(\"\\n\\n\")}`;\n}\n\n/**\n * Normalize text to a single-line preview: collapse whitespace, trim, truncate to ~200 chars.\n * Caller handles further width truncation for display. Returns \"[no text content]\" for empty input.\n */\nexport function toSingleLinePreview(text: string): string {\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\treturn toSingleLinePreview(extractCopyableText(message));\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\t// Only visible text blocks. Thinking is excluded by default and surfaced\n\t// separately via extractThinkingText (see issue 285).\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
package/docs/settings.md CHANGED
@@ -110,6 +110,24 @@ After the configured number of tool calls, dreb fires a single background LLM ca
110
110
  }
111
111
  ```
112
112
 
113
+ ### Context
114
+
115
+ | Setting | Type | Default | Description |
116
+ |---------|------|---------|-------------|
117
+ | `context.autoLoadNested` | boolean | `true` | Auto-load a directory's `AGENTS.md`/`CLAUDE.md` the first time a tool operates there |
118
+
119
+ When enabled, dreb loads nested context files that the startup upward-walk misses (e.g. a `CLAUDE.md` in a monorepo subpackage, or context in a different repo a subagent visits). The first tool to touch a directory triggers a walk up to a sensible ceiling — the session cwd for in-tree targets, otherwise the outermost git repo root, otherwise the outermost directory containing a context file — and the collected files are appended to that tool's result. Each file loads at most once per session. See [Context Files](../README.md#context-files).
120
+
121
+ **Security caution:** when working across untrusted or third-party repositories, their `AGENTS.md`/`CLAUDE.md` files may be auto-injected into the agent's context, which is a prompt-injection consideration. Set `context.autoLoadNested` to `false` to disable this behavior. Auto-loaded content is secret-scrubbed before injection, but extension `tool_result` transforms do not see it because nested context is injected after those transforms for cache safety.
122
+
123
+ ```json
124
+ {
125
+ "context": {
126
+ "autoLoadNested": true
127
+ }
128
+ }
129
+ ```
130
+
113
131
  ### Compaction
114
132
 
115
133
  | Setting | Type | Default | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreb/coding-agent",
3
- "version": "2.30.0",
3
+ "version": "2.31.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {