mixdog 0.9.21 → 0.9.23

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 (90) hide show
  1. package/README.md +1 -4
  2. package/package.json +1 -1
  3. package/scripts/channel-daemon-smoke.mjs +165 -0
  4. package/scripts/channel-daemon-stub.mjs +69 -0
  5. package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
  6. package/scripts/tool-smoke.mjs +30 -17
  7. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +187 -0
  8. package/src/runtime/agent/orchestrator/mcp/client.mjs +40 -3
  9. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
  10. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
  11. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
  15. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
  16. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
  18. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
  20. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +10 -10
  21. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +8 -6
  22. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +14 -1
  23. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +24 -11
  25. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
  26. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +23 -13
  27. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -1
  28. package/src/runtime/channels/lib/owned-runtime.mjs +126 -167
  29. package/src/runtime/channels/lib/owner-heartbeat.mjs +11 -60
  30. package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
  31. package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
  32. package/src/runtime/channels/lib/seat-lock.mjs +196 -0
  33. package/src/runtime/channels/lib/session-discovery.mjs +2 -1
  34. package/src/runtime/channels/lib/tool-dispatch.mjs +24 -8
  35. package/src/runtime/channels/lib/worker-main.mjs +42 -11
  36. package/src/runtime/memory/index.mjs +54 -7
  37. package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
  38. package/src/runtime/memory/lib/pg/process.mjs +85 -40
  39. package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
  40. package/src/runtime/shared/atomic-file.mjs +44 -10
  41. package/src/runtime/shared/tool-primitives.mjs +31 -1
  42. package/src/runtime/shared/tool-surface.mjs +23 -13
  43. package/src/session-runtime/config-lifecycle.mjs +48 -7
  44. package/src/session-runtime/lifecycle-api.mjs +9 -0
  45. package/src/session-runtime/mcp-glue.mjs +63 -1
  46. package/src/session-runtime/resource-api.mjs +62 -8
  47. package/src/session-runtime/runtime-core.mjs +32 -2
  48. package/src/session-runtime/session-text.mjs +41 -0
  49. package/src/session-runtime/session-turn-api.mjs +24 -0
  50. package/src/session-runtime/settings-api.mjs +8 -1
  51. package/src/session-runtime/tool-catalog.mjs +306 -38
  52. package/src/session-runtime/tool-defs.mjs +7 -7
  53. package/src/session-runtime/workflow.mjs +2 -1
  54. package/src/standalone/channel-daemon-client.mjs +224 -0
  55. package/src/standalone/channel-daemon-transport.mjs +351 -0
  56. package/src/standalone/channel-daemon.mjs +139 -0
  57. package/src/standalone/channel-worker.mjs +213 -4
  58. package/src/standalone/hook-bus.mjs +71 -3
  59. package/src/tui/App.jsx +105 -17
  60. package/src/tui/app/clipboard.mjs +39 -19
  61. package/src/tui/app/doctor.mjs +57 -0
  62. package/src/tui/app/extension-pickers.mjs +53 -9
  63. package/src/tui/app/maintenance-pickers.mjs +0 -5
  64. package/src/tui/app/slash-dispatch.mjs +4 -4
  65. package/src/tui/app/text-layout.mjs +11 -0
  66. package/src/tui/app/use-mouse-input.mjs +235 -51
  67. package/src/tui/app/use-prompt-handlers.mjs +49 -30
  68. package/src/tui/app/use-transcript-scroll.mjs +124 -27
  69. package/src/tui/app/use-transcript-window.mjs +55 -1
  70. package/src/tui/components/Message.jsx +1 -1
  71. package/src/tui/components/PromptInput.jsx +3 -1
  72. package/src/tui/components/QueuedCommands.jsx +21 -10
  73. package/src/tui/components/ToolExecution.jsx +2 -2
  74. package/src/tui/dist/index.mjs +653 -310
  75. package/src/tui/engine/session-api.mjs +17 -7
  76. package/src/tui/engine/session-flow.mjs +6 -0
  77. package/src/tui/engine.mjs +8 -0
  78. package/src/tui/index.jsx +62 -18
  79. package/src/tui/paste-attachments.mjs +26 -0
  80. package/src/ui/statusline.mjs +45 -5
  81. package/src/ui/tool-card.mjs +8 -1
  82. package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
  83. package/src/workflows/bench/WORKFLOW.md +46 -0
  84. package/src/workflows/default/WORKFLOW.md +6 -0
  85. package/src/workflows/solo/WORKFLOW.md +5 -0
  86. package/vendor/ink/build/ink.js +23 -1
  87. package/vendor/ink/build/output.js +154 -71
  88. package/vendor/ink/build/render-node-to-output.js +44 -2
  89. package/vendor/ink/build/render.js +4 -0
  90. package/vendor/ink/build/renderer.js +4 -1
@@ -156,10 +156,11 @@ var require_mixdog_debug = __commonJS({
156
156
  // src/tui/index.jsx
157
157
  import React21 from "react";
158
158
  import { render } from "../../../vendor/ink/build/index.js";
159
- import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync7, openSync as openSync2, readSync } from "node:fs";
160
- import { tmpdir as tmpdir3 } from "node:os";
161
- import { dirname as dirname9, join as join11 } from "node:path";
159
+ import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync6, openSync as openSync2, readSync } from "node:fs";
160
+ import { tmpdir as tmpdir2 } from "node:os";
161
+ import { dirname as dirname9, join as join10 } from "node:path";
162
162
  import { performance as performance3 } from "node:perf_hooks";
163
+ import { format } from "node:util";
163
164
 
164
165
  // src/tui/App.jsx
165
166
  import React20, { useState as useState8, useCallback as useCallback6, useEffect as useEffect10, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef9 } from "react";
@@ -1788,8 +1789,21 @@ function parseMcpToolName(name) {
1788
1789
  function isMcpToolName(name) {
1789
1790
  return Boolean(parseMcpToolName(name));
1790
1791
  }
1792
+ var SELF_MCP_SERVER = "plugin_mixdog_mixdog";
1793
+ function isSelfMcpToolName(name) {
1794
+ const mcp = parseMcpToolName(name);
1795
+ return Boolean(mcp && mcp.server === SELF_MCP_SERVER);
1796
+ }
1797
+ function isExternalMcpToolName(name) {
1798
+ return isMcpToolName(name) && !isSelfMcpToolName(name);
1799
+ }
1800
+ function titleCaseMcpServer(server) {
1801
+ return String(server || "").split(/[_\s-]+/).filter(Boolean).map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1).toLowerCase()}`).join(" ");
1802
+ }
1803
+ var TOOL_NAME_ALIASES = { tool_search: "load_tool" };
1791
1804
  function normalizeToolName(name) {
1792
- return stripToolPrefix(name).replace(/-/g, "_").toLowerCase();
1805
+ const base = stripToolPrefix(name).replace(/-/g, "_").toLowerCase();
1806
+ return TOOL_NAME_ALIASES[base] || base;
1793
1807
  }
1794
1808
  function truncateToolText(value, max = DEFAULT_SUMMARY_MAX) {
1795
1809
  const text = String(value ?? "").trim();
@@ -1829,16 +1843,6 @@ function displayToolPath(path2) {
1829
1843
  function compactParts(parts) {
1830
1844
  return parts.filter((part) => part != null && String(part).trim()).map((part) => String(part).trim()).join(STATUS_SEPARATOR);
1831
1845
  }
1832
- function compactSlash(left, right) {
1833
- const a = String(left ?? "").trim();
1834
- const b = String(right ?? "").trim();
1835
- return a && b ? `${a}/${b}` : a || b;
1836
- }
1837
- function mcpToolTarget(name, max = DEFAULT_SUMMARY_MAX) {
1838
- const mcp = parseMcpToolName(name);
1839
- if (!mcp) return "";
1840
- return truncateToolText(compactSlash(mcp.server, mcp.tool), max);
1841
- }
1842
1846
  function quoted(value, max) {
1843
1847
  const text = truncateToolText(value || "", max);
1844
1848
  return text ? `"${text}"` : "";
@@ -2435,7 +2439,10 @@ function summarizeAgentSurfaceBrief(name, args, resultText, { isError = false, i
2435
2439
 
2436
2440
  // src/runtime/shared/tool-surface.mjs
2437
2441
  function displayToolName(name, args = {}) {
2438
- if (isMcpToolName(name)) return "MCP";
2442
+ if (isExternalMcpToolName(name)) {
2443
+ const mcp = parseMcpToolName(name);
2444
+ return `MCP ${titleCaseMcpServer(mcp.server)}`;
2445
+ }
2439
2446
  const normalized = normalizeToolName(name);
2440
2447
  switch (normalized) {
2441
2448
  case "read":
@@ -2461,7 +2468,7 @@ function displayToolName(name, args = {}) {
2461
2468
  case "list":
2462
2469
  case "ls":
2463
2470
  return "Search";
2464
- case "tool_search":
2471
+ case "load_tool":
2465
2472
  return toolSearchDisplayLabel(parseToolArgs(args));
2466
2473
  case "search":
2467
2474
  case "search_query":
@@ -2511,10 +2518,10 @@ function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}) {
2511
2518
  const a = parseToolArgs(args);
2512
2519
  if (!a || typeof a !== "object") return "";
2513
2520
  const normalized = normalizeToolName(name);
2514
- const mcpTarget = mcpToolTarget(name, max);
2515
- if (mcpTarget) {
2521
+ if (isExternalMcpToolName(name)) {
2522
+ const mcp = parseMcpToolName(name);
2516
2523
  return compactParts([
2517
- mcpTarget,
2524
+ truncateToolText(mcp.tool, max),
2518
2525
  truncateToolText(firstText(a.query, a.q, a.text, a.prompt, a.path, a.uri, a.name, a.id, a.action), Math.min(max, 80))
2519
2526
  ]);
2520
2527
  }
@@ -2591,8 +2598,8 @@ function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}) {
2591
2598
  return formatCountedUnit(collectionCount(a.query, a.prompt, a.task, a.goal), "query", "queries");
2592
2599
  }
2593
2600
  return truncateSingleLine(firstText(a.query, a.prompt, a.task, a.goal, a.path), Math.min(max, 80));
2594
- case "tool_search": {
2595
- const selected = splitToolSearchSelection(a.select);
2601
+ case "load_tool": {
2602
+ const selected = [...splitToolSearchSelection(a.names), ...splitToolSearchSelection(a.select)];
2596
2603
  if (selected.length) return truncateToolText(selected.map(displayToolSearchTarget).join(", "), max);
2597
2604
  return quoted(firstText(a.query, a.q, a.text), max);
2598
2605
  }
@@ -2695,7 +2702,7 @@ var TOOL_CATEGORY = /* @__PURE__ */ new Map([
2695
2702
  ["glob", "Search"],
2696
2703
  ["list", "Search"],
2697
2704
  ["ls", "Search"],
2698
- ["tool_search", "Load"],
2705
+ ["load_tool", "Load"],
2699
2706
  ["search", "Web Research"],
2700
2707
  ["web_search", "Web Research"],
2701
2708
  ["search_query", "Web Research"],
@@ -2732,7 +2739,7 @@ var TOOL_CATEGORY = /* @__PURE__ */ new Map([
2732
2739
  ["use_skill", "Skill"]
2733
2740
  ]);
2734
2741
  function classifyToolCategory(name, args = {}) {
2735
- if (isMcpToolName(name)) return "MCP";
2742
+ if (isExternalMcpToolName(name)) return "MCP";
2736
2743
  const normalized = normalizeToolName(name);
2737
2744
  if (normalized === "code_graph") {
2738
2745
  const mode = String(args.mode || args.action || "").toLowerCase();
@@ -2780,8 +2787,12 @@ function toolWorkUnit(name, args = {}, category = "") {
2780
2787
  const a = parseToolArgs(args);
2781
2788
  const normalized = normalizeToolName(name);
2782
2789
  const cat = category || classifyToolCategory(name, a);
2783
- if (isMcpToolName(name)) {
2784
- return unitDescriptor("MCP", { count: queryCount(a, "query", "q", "text", "prompt", "path", "uri", "name", "id", "action") || 1 });
2790
+ if (isExternalMcpToolName(name)) {
2791
+ const mcp = parseMcpToolName(name);
2792
+ return unitDescriptor("MCP", {
2793
+ count: queryCount(a, "query", "q", "text", "prompt", "path", "uri", "name", "id", "action") || 1,
2794
+ noun: `${titleCaseMcpServer(mcp.server)} tool`
2795
+ });
2785
2796
  }
2786
2797
  switch (normalized) {
2787
2798
  case "read":
@@ -2816,8 +2827,8 @@ function toolWorkUnit(name, args = {}, category = "") {
2816
2827
  case "list":
2817
2828
  case "ls":
2818
2829
  return unitDescriptor("Search", { count: queryCount(a, "path", "paths", "dir", "dirs", "cwd") || 1, active: "Listing", done: "Listed", noun: "directory", pluralNoun: "directories" });
2819
- case "tool_search": {
2820
- const selected = splitToolSearchSelection(a.select);
2830
+ case "load_tool": {
2831
+ const selected = [...splitToolSearchSelection(a.names), ...splitToolSearchSelection(a.select)];
2821
2832
  if (selected.length) return unitDescriptor("Load", { count: selected.length, noun: "tool" });
2822
2833
  return unitDescriptor("Load", { count: queryCount(a, "query", "q", "text") || 1, noun: "query", pluralNoun: "queries" });
2823
2834
  }
@@ -4943,7 +4954,7 @@ function PromptInput({
4943
4954
  return;
4944
4955
  }
4945
4956
  if (!commandPaletteActive && (key.ctrl && inputKey === "v" || key.meta && inputKey === "v")) {
4946
- handleExternalPaste("", { source: "clipboard-image-shortcut" });
4957
+ handleExternalPaste("", { source: "clipboard-shortcut" });
4947
4958
  return;
4948
4959
  }
4949
4960
  if (key.return) {
@@ -5220,13 +5231,17 @@ ${d.value.slice(d.cursor)}`,
5220
5231
  import React4 from "react";
5221
5232
  import { Box as Box4, Text as Text4 } from "../../../vendor/ink/build/index.js";
5222
5233
  import { jsx as jsx4 } from "react/jsx-runtime";
5223
- function QueuedCommands({ queued, columns }) {
5234
+ function QueuedCommands({ queued, columns, compact = false }) {
5224
5235
  if (!queued || queued.length === 0) return null;
5225
5236
  const bandColumns = Math.max(1, columns - 1);
5237
+ const contentWidth = Math.max(1, columns - 4);
5226
5238
  return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: queued.map((item) => {
5227
- const contentWidth = Math.max(1, columns - 4);
5228
5239
  const sourceText = String(item.displayText || item.text || "");
5229
- const displayText = sourceText.length > contentWidth ? contentWidth <= 1 ? "\u2026".repeat(contentWidth) : sourceText.slice(0, Math.max(1, contentWidth - 1)) + "\u2026" : sourceText;
5240
+ let displayText = sourceText;
5241
+ if (compact) {
5242
+ const oneLine2 = sourceText.replace(/\r?\n/g, " ");
5243
+ displayText = oneLine2.length > contentWidth ? contentWidth <= 1 ? "\u2026".repeat(contentWidth) : oneLine2.slice(0, Math.max(1, contentWidth - 1)) + "\u2026" : oneLine2;
5244
+ }
5230
5245
  return /* @__PURE__ */ jsx4(Box4, { width: bandColumns, backgroundColor: theme.userMessageBackground, paddingLeft: 2, paddingRight: 1, children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: /* @__PURE__ */ jsx4(Text4, { color: theme.mixdogIvory, children: displayText }) }) }, item.id);
5231
5246
  }) });
5232
5247
  }
@@ -6346,6 +6361,12 @@ function wrappedDetailRows(text, width) {
6346
6361
  const w = Math.max(1, Math.floor(Number(width) || 1));
6347
6362
  return wrapAnsi(value, w, { trim: false, hard: true }).split("\n").length;
6348
6363
  }
6364
+ function queuedBandRows(text, width) {
6365
+ const value = String(text ?? "");
6366
+ if (!value) return 1;
6367
+ const w = Math.max(1, Math.floor(Number(width) || 1));
6368
+ return Math.max(1, wrapAnsi(value, w, { trim: false, hard: true }).split("\n").length);
6369
+ }
6349
6370
  function visualRowStartOffsets(text, width) {
6350
6371
  const value = String(text ?? "");
6351
6372
  const w = Math.max(1, Math.floor(Number(width) || 1));
@@ -6830,6 +6851,7 @@ function TextEntryPanel({
6830
6851
 
6831
6852
  // src/tui/paste-attachments.mjs
6832
6853
  import { execFile } from "node:child_process";
6854
+ import { Buffer as Buffer2 } from "node:buffer";
6833
6855
  import { randomBytes } from "node:crypto";
6834
6856
  import { existsSync, unlinkSync } from "node:fs";
6835
6857
  import { readFile as readFileAsync, stat as statAsync } from "node:fs/promises";
@@ -6983,7 +7005,7 @@ function mimeForPath(path2) {
6983
7005
  return IMAGE_MIME[extname(String(path2 || "")).toLowerCase()] || null;
6984
7006
  }
6985
7007
  function execFileBuffer(cmd, args, options = {}) {
6986
- return new Promise((resolve6) => {
7008
+ return new Promise((resolve5) => {
6987
7009
  execFile(cmd, args, {
6988
7010
  windowsHide: true,
6989
7011
  encoding: "buffer",
@@ -6991,7 +7013,7 @@ function execFileBuffer(cmd, args, options = {}) {
6991
7013
  timeout: 5e3,
6992
7014
  ...options
6993
7015
  }, (error, stdout, stderr) => {
6994
- resolve6({ ok: !error, code: error?.code ?? 0, stdout, stderr, error });
7016
+ resolve5({ ok: !error, code: error?.code ?? 0, stdout, stderr, error });
6995
7017
  });
6996
7018
  });
6997
7019
  }
@@ -7066,7 +7088,7 @@ function splitPastedImagePathCandidates(text) {
7066
7088
  return out;
7067
7089
  }
7068
7090
  async function imageAttachmentFromBuffer(buffer, mimeType, { filename = "Pasted image", sourcePath = "" } = {}) {
7069
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error("image is empty");
7091
+ if (!Buffer2.isBuffer(buffer) || buffer.length === 0) throw new Error("image is empty");
7070
7092
  const ext = (mimeType || "image/png").split("/")[1] || "png";
7071
7093
  const resized = await resizeImageBuffer(buffer, ext);
7072
7094
  if (resized) {
@@ -7145,6 +7167,30 @@ async function readClipboardImageAttachment() {
7145
7167
  }
7146
7168
  }
7147
7169
  }
7170
+ async function readClipboardText() {
7171
+ if (process.platform === "win32") {
7172
+ const ps = "$t=Get-Clipboard -Raw; if ($null -eq $t) { exit 2 }; [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($t)))";
7173
+ const r = await execFileBuffer("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", ps], { timeout: 3e3 });
7174
+ if (!r.ok || !r.stdout?.length) return "";
7175
+ try {
7176
+ return Buffer2.from(r.stdout.toString("ascii").trim(), "base64").toString("utf8");
7177
+ } catch {
7178
+ return "";
7179
+ }
7180
+ }
7181
+ if (process.platform === "darwin") {
7182
+ const r = await execFileBuffer("pbpaste", [], { timeout: 3e3 });
7183
+ return r.ok && r.stdout?.length ? r.stdout.toString("utf8") : "";
7184
+ }
7185
+ if (process.platform === "linux") {
7186
+ const wl = await execFileBuffer("wl-paste", ["--no-newline"], { timeout: 3e3 });
7187
+ if (wl.ok && wl.stdout?.length) return wl.stdout.toString("utf8");
7188
+ const xc = await execFileBuffer("xclip", ["-selection", "clipboard", "-o"], { timeout: 3e3 });
7189
+ if (xc.ok && xc.stdout?.length) return xc.stdout.toString("utf8");
7190
+ return "";
7191
+ }
7192
+ return "";
7193
+ }
7148
7194
 
7149
7195
  // src/standalone/projects.mjs
7150
7196
  import { homedir } from "node:os";
@@ -7302,12 +7348,12 @@ import { spawn } from "node:child_process";
7302
7348
  import path from "node:path";
7303
7349
  var DIALOG_TIMEOUT_MS = 5 * 60 * 1e3;
7304
7350
  function runCapture(cmd, args, { timeoutMs = DIALOG_TIMEOUT_MS } = {}) {
7305
- return new Promise((resolve6) => {
7351
+ return new Promise((resolve5) => {
7306
7352
  let child;
7307
7353
  try {
7308
7354
  child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
7309
7355
  } catch (error) {
7310
- resolve6({ ok: false, code: -1, stdout: "", error });
7356
+ resolve5({ ok: false, code: -1, stdout: "", error });
7311
7357
  return;
7312
7358
  }
7313
7359
  let stdout = "";
@@ -7319,7 +7365,7 @@ function runCapture(cmd, args, { timeoutMs = DIALOG_TIMEOUT_MS } = {}) {
7319
7365
  child.kill();
7320
7366
  } catch {
7321
7367
  }
7322
- resolve6({ ok: false, code: -1, stdout: "", error: new Error("dialog timed out") });
7368
+ resolve5({ ok: false, code: -1, stdout: "", error: new Error("dialog timed out") });
7323
7369
  }, timeoutMs);
7324
7370
  timer.unref?.();
7325
7371
  child.stdout.on("data", (chunk) => {
@@ -7329,13 +7375,13 @@ function runCapture(cmd, args, { timeoutMs = DIALOG_TIMEOUT_MS } = {}) {
7329
7375
  if (settled) return;
7330
7376
  settled = true;
7331
7377
  clearTimeout(timer);
7332
- resolve6({ ok: false, code: -1, stdout: "", error });
7378
+ resolve5({ ok: false, code: -1, stdout: "", error });
7333
7379
  });
7334
7380
  child.on("close", (code) => {
7335
7381
  if (settled) return;
7336
7382
  settled = true;
7337
7383
  clearTimeout(timer);
7338
- resolve6({ ok: code === 0, code, stdout });
7384
+ resolve5({ ok: code === 0, code, stdout });
7339
7385
  });
7340
7386
  });
7341
7387
  }
@@ -7343,10 +7389,10 @@ function spawnFailed(result) {
7343
7389
  return !!result && result.code === -1;
7344
7390
  }
7345
7391
  function commandExists(cmd) {
7346
- return new Promise((resolve6) => {
7392
+ return new Promise((resolve5) => {
7347
7393
  const probe = process.platform === "win32" ? spawn("where", [cmd], { stdio: "ignore", windowsHide: true }) : spawn("which", [cmd], { stdio: "ignore" });
7348
- probe.on("error", () => resolve6(false));
7349
- probe.on("close", (code) => resolve6(code === 0));
7394
+ probe.on("error", () => resolve5(false));
7395
+ probe.on("close", (code) => resolve5(code === 0));
7350
7396
  });
7351
7397
  }
7352
7398
  function psSingleQuoted(value) {
@@ -7845,64 +7891,67 @@ function memoryCoreResultErrorText(text) {
7845
7891
  }
7846
7892
 
7847
7893
  // src/tui/app/clipboard.mjs
7848
- import { Buffer as Buffer2 } from "node:buffer";
7894
+ import { Buffer as Buffer3 } from "node:buffer";
7849
7895
  import { spawn as spawn2 } from "node:child_process";
7850
7896
  function osc52ClipboardSequence(text) {
7851
- const b64 = Buffer2.from(String(text ?? ""), "utf8").toString("base64");
7897
+ const b64 = Buffer3.from(String(text ?? ""), "utf8").toString("base64");
7852
7898
  const raw = `\x1B]52;c;${b64}\x07`;
7853
7899
  if (!process.env.TMUX) return raw;
7854
7900
  return `\x1BPtmux;${raw.replaceAll("\x1B", "\x1B\x1B")}\x1B\\`;
7855
7901
  }
7902
+ var OSC52_MAX_BYTES = 256 * 1024;
7856
7903
  function writeOsc52Clipboard(text) {
7904
+ const value = String(text ?? "");
7905
+ if (Buffer3.byteLength(value, "utf8") > OSC52_MAX_BYTES) return false;
7857
7906
  try {
7858
- process.stdout.write(osc52ClipboardSequence(text));
7907
+ process.stdout.write(osc52ClipboardSequence(value));
7859
7908
  return true;
7860
7909
  } catch {
7861
7910
  return false;
7862
7911
  }
7863
7912
  }
7864
7913
  function nativeClipboardCommand(text) {
7914
+ const value = String(text ?? "");
7865
7915
  if (process.platform === "win32") {
7866
7916
  return {
7867
- cmd: "powershell.exe",
7868
- args: [
7869
- "-NoLogo",
7870
- "-NoProfile",
7871
- "-NonInteractive",
7872
- "-Command",
7873
- "$b=[Console]::In.ReadToEnd();$t=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b));Set-Clipboard -Value $t"
7874
- ],
7875
- input: Buffer2.from(String(text ?? ""), "utf8").toString("base64")
7917
+ cmd: "clip.exe",
7918
+ args: [],
7919
+ input: Buffer3.from(value, "utf16le")
7876
7920
  };
7877
7921
  }
7878
- if (process.platform === "darwin") return { cmd: "pbcopy", args: [], input: text };
7879
- if (process.env.WAYLAND_DISPLAY) return { cmd: "wl-copy", args: [], input: text };
7880
- return { cmd: "xclip", args: ["-selection", "clipboard"], input: text };
7922
+ if (process.platform === "darwin") return { cmd: "pbcopy", args: [], input: value };
7923
+ if (process.env.WAYLAND_DISPLAY) return { cmd: "wl-copy", args: [], input: value };
7924
+ return { cmd: "xclip", args: ["-selection", "clipboard"], input: value };
7881
7925
  }
7882
7926
  function copyToClipboard(text) {
7883
7927
  const value = String(text ?? "");
7884
7928
  const wroteOsc52 = writeOsc52Clipboard(value);
7885
- return new Promise((resolve6, reject) => {
7929
+ return new Promise((resolve5, reject) => {
7886
7930
  const { cmd, args, input } = nativeClipboardCommand(value);
7887
7931
  let child;
7888
7932
  try {
7889
7933
  child = spawn2(cmd, args, { stdio: ["pipe", "ignore", "ignore"], windowsHide: true });
7890
7934
  } catch (e) {
7891
- if (wroteOsc52) resolve6();
7935
+ if (wroteOsc52) resolve5();
7892
7936
  else reject(e);
7893
7937
  return;
7894
7938
  }
7895
7939
  child.on("error", (e) => {
7896
- if (wroteOsc52) resolve6();
7897
- else reject(e);
7898
- });
7899
- child.on("close", (code) => {
7900
- if (code === 0 || wroteOsc52) resolve6();
7901
- else reject(new Error(`${cmd} exited with code ${code}`));
7940
+ if (!wroteOsc52) reject(e);
7902
7941
  });
7942
+ if (!wroteOsc52) {
7943
+ child.on("close", (code) => {
7944
+ if (code === 0) resolve5();
7945
+ else reject(new Error(`${cmd} exited with code ${code}`));
7946
+ });
7947
+ }
7903
7948
  child.stdin.on("error", () => {
7904
7949
  });
7905
7950
  child.stdin.end(input);
7951
+ if (wroteOsc52) {
7952
+ child.unref?.();
7953
+ resolve5();
7954
+ }
7906
7955
  });
7907
7956
  }
7908
7957
 
@@ -10382,6 +10431,7 @@ var ALT_SCROLL_ON = "\x1B[?1007h";
10382
10431
  var MOUSE_MODIFIER_MASK = 4 | 8 | 16;
10383
10432
  var MOUSE_CTRL_MASK = 16;
10384
10433
  var MOUSE_SHIFT_MASK = 4;
10434
+ var IS_WINDOWS_TERMINAL = Boolean(process.env.WT_SESSION);
10385
10435
  function useMouseInput({
10386
10436
  inkInput,
10387
10437
  isRawModeSupported,
@@ -10409,7 +10459,8 @@ function useMouseInput({
10409
10459
  setMeasuredRowsVersion,
10410
10460
  clearStitchBuffer
10411
10461
  }) {
10412
- const mouseZoomPassthroughTimerRef = useRef6(null);
10462
+ const zoomTimerRef = useRef6(null);
10463
+ const edgeAutoscrollRef = useRef6({ dir: 0, timer: null, noMove: 0 });
10413
10464
  const passthroughCtrlWheelZoom = useCallback2(() => {
10414
10465
  if (!stdout?.write) return;
10415
10466
  try {
@@ -10417,19 +10468,19 @@ function useMouseInput({
10417
10468
  } catch {
10418
10469
  return;
10419
10470
  }
10420
- if (mouseZoomPassthroughTimerRef.current) clearTimeout(mouseZoomPassthroughTimerRef.current);
10421
- mouseZoomPassthroughTimerRef.current = setTimeout(() => {
10422
- mouseZoomPassthroughTimerRef.current = null;
10471
+ if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
10472
+ zoomTimerRef.current = setTimeout(() => {
10473
+ zoomTimerRef.current = null;
10423
10474
  try {
10424
10475
  stdout.write(ALT_SCROLL_ON + MOUSE_TRACKING_ON);
10425
10476
  } catch {
10426
10477
  }
10427
10478
  }, 700);
10428
- mouseZoomPassthroughTimerRef.current.unref?.();
10429
- }, [stdout]);
10479
+ zoomTimerRef.current.unref?.();
10480
+ }, [stdout, zoomTimerRef]);
10430
10481
  useEffect6(() => () => {
10431
- if (mouseZoomPassthroughTimerRef.current) clearTimeout(mouseZoomPassthroughTimerRef.current);
10432
- }, []);
10482
+ if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
10483
+ }, [zoomTimerRef]);
10433
10484
  useEffect6(() => {
10434
10485
  if (!inkInput || !isRawModeSupported) return void 0;
10435
10486
  const WHEEL_SGR = /\x1b\[<(\d+);/;
@@ -10494,6 +10545,65 @@ function useMouseInput({
10494
10545
  promptMouseSelectionRef.current?.clear?.();
10495
10546
  applySelectionRect(null);
10496
10547
  };
10548
+ const EDGE_AUTOSCROLL_INTERVAL_MS = 50;
10549
+ const stopEdgeAutoscroll = () => {
10550
+ const st = edgeAutoscrollRef.current;
10551
+ if (st.timer) {
10552
+ clearInterval(st.timer);
10553
+ st.timer = null;
10554
+ }
10555
+ st.dir = 0;
10556
+ st.noMove = 0;
10557
+ };
10558
+ const startEdgeAutoscroll = (dir) => {
10559
+ const st = edgeAutoscrollRef.current;
10560
+ if (st.dir === dir && st.timer) return;
10561
+ stopEdgeAutoscroll();
10562
+ st.dir = dir;
10563
+ st.noMove = 0;
10564
+ st.timer = setInterval(() => {
10565
+ const drag = dragRef.current;
10566
+ const st2 = edgeAutoscrollRef.current;
10567
+ if (!drag.active || drag.region !== "transcript" || st2.dir === 0) {
10568
+ stopEdgeAutoscroll();
10569
+ return;
10570
+ }
10571
+ const before = Number(scrollTargetRef.current) || 0;
10572
+ queueScrollCoalesced(st2.dir * 3);
10573
+ if ((Number(scrollTargetRef.current) || 0) !== before) {
10574
+ st2.noMove = 0;
10575
+ } else if (++st2.noMove >= 3) {
10576
+ stopEdgeAutoscroll();
10577
+ }
10578
+ }, EDGE_AUTOSCROLL_INTERVAL_MS);
10579
+ st.timer.unref?.();
10580
+ };
10581
+ const finalizeActiveDrag = (fx, fy) => {
10582
+ const drag = dragRef.current;
10583
+ if (!drag.active) return;
10584
+ stopEdgeAutoscroll();
10585
+ const region = drag.region;
10586
+ if (region === "prompt") {
10587
+ const offset = promptOffsetAt(fx, fy);
10588
+ drag.active = false;
10589
+ promptMouseSelectionRef.current?.extendTo?.(offset, true);
10590
+ return;
10591
+ }
10592
+ const span = drag.anchorSpan;
10593
+ const finalY = region === "status" ? clampToStatusBand(fy) : clampToTranscriptViewport(fy);
10594
+ drag.active = false;
10595
+ if (span) {
10596
+ const rect = buildSpanRect(span, fx, finalY, region, drag.anchorScroll);
10597
+ applySelectionRect(rect);
10598
+ } else {
10599
+ const anchor = region === "status" ? drag.anchor : selectionPointAtCurrentScroll(drag.anchor, drag.anchorScroll);
10600
+ const rect = linearSelection(anchor, { x: fx, y: finalY });
10601
+ const empty = rect.x1 === rect.x2 && rect.y1 === rect.y2;
10602
+ if (empty) applySelectionRect(null);
10603
+ else applySelectionRect(rect);
10604
+ }
10605
+ if (TRANSCRIPT_MEASURED_ROWS) setMeasuredRowsVersion((v) => (v + 1) % 1e6);
10606
+ };
10497
10607
  const onMouse = (event) => {
10498
10608
  if (!event || typeof event !== "object") return;
10499
10609
  let up = 0;
@@ -10505,15 +10615,20 @@ function useMouseInput({
10505
10615
  const wm = WHEEL_SGR.exec(seq);
10506
10616
  const ctrl = wm ? (Number(wm[1]) & MOUSE_CTRL_MASK) !== 0 : false;
10507
10617
  if (ctrl) {
10618
+ if (dragRef.current.active) {
10619
+ const last = dragRef.current.last || {};
10620
+ finalizeActiveDrag(Number(last.x) || 0, Number(last.y) || 0);
10621
+ } else {
10622
+ stopEdgeAutoscroll();
10623
+ }
10508
10624
  passthroughCtrlWheelZoom();
10509
10625
  return;
10510
10626
  }
10511
10627
  if (name === "wheelup") up += 1;
10512
10628
  else down += 1;
10513
10629
  if (up !== 0 || down !== 0) {
10514
- if (dragRef.current.active) return;
10515
10630
  const palette = slashPaletteRef.current;
10516
- if (palette.open && palette.count > 0) {
10631
+ if (!dragRef.current.active && palette.open && palette.count > 0) {
10517
10632
  const step = down - up;
10518
10633
  if (step !== 0) {
10519
10634
  setSlashIndex((index) => Math.max(0, Math.min(palette.count - 1, index + step)));
@@ -10535,14 +10650,64 @@ function useMouseInput({
10535
10650
  const baseButton = button & 3;
10536
10651
  const isMotion = (button & 32) !== 0;
10537
10652
  const shiftHeld = (button & MOUSE_SHIFT_MASK) !== 0;
10653
+ const ctrlHeld = (button & MOUSE_CTRL_MASK) !== 0;
10654
+ if (IS_WINDOWS_TERMINAL && shiftHeld) return;
10655
+ if (IS_WINDOWS_TERMINAL && press && !isMotion) {
10656
+ store.forceRenderRepaint?.();
10657
+ }
10658
+ const extendHeld = shiftHeld || ctrlHeld;
10659
+ const isRightPress = baseButton === 2 && press && !isMotion;
10660
+ if (isRightPress) {
10661
+ if (isInPromptBox(x, y)) {
10662
+ const ctl = promptMouseSelectionRef.current;
10663
+ if (ctl?.hasSelection?.()) {
10664
+ applySelectionRect(null);
10665
+ const offset = promptOffsetAt(x, y);
10666
+ stopSmoothScroll();
10667
+ dragRef.current = { anchor: { x, y }, anchorScroll: 0, last: { x, y }, active: false, rect: null, region: "prompt", anchorSpan: null };
10668
+ if (offset != null) ctl.extendTo?.(offset, true);
10669
+ lastClickRef.current = { x: -1, y: -1, t: 0 };
10670
+ }
10671
+ return;
10672
+ }
10673
+ const inTranscriptR = isInTranscriptViewport(y);
10674
+ const inStatusR = !inTranscriptR && isInStatusBand(y);
10675
+ if (!inTranscriptR && !inStatusR) return;
10676
+ const regionR = inTranscriptR ? "transcript" : "status";
10677
+ const nowR = Date.now();
10678
+ if (dragRef.current.region === regionR && dragRef.current.anchorSpan) {
10679
+ promptMouseSelectionRef.current?.clear?.();
10680
+ const selectionY = regionR === "status" ? clampToStatusBand(y) : clampToTranscriptViewport(y);
10681
+ const span = dragRef.current.anchorSpan;
10682
+ const rect = buildSpanRect(span, x, selectionY, regionR, dragRef.current.anchorScroll);
10683
+ stopSmoothScroll();
10684
+ dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
10685
+ applySelectionRect(rect);
10686
+ lastClickRef.current = { x, y, t: nowR, count: 1 };
10687
+ return;
10688
+ }
10689
+ if (dragRef.current.region === regionR && !dragRef.current.anchorSpan && dragRef.current.anchor && dragRef.current.rect && !(dragRef.current.rect.x1 === dragRef.current.rect.x2 && dragRef.current.rect.y1 === dragRef.current.rect.y2)) {
10690
+ promptMouseSelectionRef.current?.clear?.();
10691
+ const selectionY = regionR === "status" ? clampToStatusBand(y) : clampToTranscriptViewport(y);
10692
+ const anchor = regionR === "status" ? dragRef.current.anchor : selectionPointAtCurrentScroll(dragRef.current.anchor, dragRef.current.anchorScroll);
10693
+ const rect = linearSelection(anchor, { x, y: selectionY });
10694
+ stopSmoothScroll();
10695
+ dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
10696
+ applySelectionRect(rect);
10697
+ lastClickRef.current = { x, y, t: nowR, count: 1 };
10698
+ return;
10699
+ }
10700
+ return;
10701
+ }
10538
10702
  if (baseButton === 0 && press && !isMotion) {
10703
+ stopEdgeAutoscroll();
10539
10704
  if (isInPromptBox(x, y)) {
10540
10705
  applySelectionRect(null);
10541
10706
  const offset = promptOffsetAt(x, y);
10542
10707
  stopSmoothScroll();
10543
10708
  dragRef.current = { anchor: { x, y }, anchorScroll: 0, last: { x, y }, active: true, rect: null, region: "prompt", anchorSpan: null };
10544
10709
  const ctl = promptMouseSelectionRef.current;
10545
- if (shiftHeld && ctl?.hasSelection?.()) {
10710
+ if (extendHeld && ctl?.hasSelection?.()) {
10546
10711
  if (offset != null) ctl.extendTo?.(offset, true);
10547
10712
  lastClickRef.current = { x: -1, y: -1, t: 0 };
10548
10713
  return;
@@ -10577,7 +10742,22 @@ function useMouseInput({
10577
10742
  const region = inTranscript ? "transcript" : "status";
10578
10743
  promptMouseSelectionRef.current?.clear?.();
10579
10744
  const now = Date.now();
10580
- if (shiftHeld && dragRef.current.region === region && !dragRef.current.anchorSpan && dragRef.current.anchor && dragRef.current.rect && !(dragRef.current.rect.x1 === dragRef.current.rect.x2 && dragRef.current.rect.y1 === dragRef.current.rect.y2)) {
10745
+ if (extendHeld && dragRef.current.region === region && dragRef.current.anchorSpan) {
10746
+ const selectionY = region === "status" ? clampToStatusBand(y) : clampToTranscriptViewport(y);
10747
+ const span = dragRef.current.anchorSpan;
10748
+ const rect = buildSpanRect(span, x, selectionY, region, dragRef.current.anchorScroll);
10749
+ stopSmoothScroll();
10750
+ dragRef.current = {
10751
+ ...dragRef.current,
10752
+ last: { x, y: selectionY },
10753
+ active: true,
10754
+ region
10755
+ };
10756
+ applySelectionRect(rect);
10757
+ lastClickRef.current = { x, y, t: now, count: 1 };
10758
+ return;
10759
+ }
10760
+ if (extendHeld && dragRef.current.region === region && !dragRef.current.anchorSpan && dragRef.current.anchor && dragRef.current.rect && !(dragRef.current.rect.x1 === dragRef.current.rect.x2 && dragRef.current.rect.y1 === dragRef.current.rect.y2)) {
10581
10761
  const selectionY = region === "status" ? clampToStatusBand(y) : clampToTranscriptViewport(y);
10582
10762
  const anchor = region === "status" ? dragRef.current.anchor : selectionPointAtCurrentScroll(dragRef.current.anchor, dragRef.current.anchorScroll);
10583
10763
  const rect = linearSelection(anchor, { x, y: selectionY });
@@ -10656,41 +10836,23 @@ function useMouseInput({
10656
10836
  const { top, bottom } = transcriptViewport();
10657
10837
  if (y <= 1 && (y < prevDragY || y < top)) {
10658
10838
  queueScrollCoalesced(3);
10839
+ startEdgeAutoscroll(1);
10659
10840
  } else if (y >= frameRows - 5 && (y > prevDragY || y > bottom)) {
10660
10841
  queueScrollCoalesced(-3);
10661
- }
10662
- }
10663
- } else if (!press && dragRef.current.active) {
10664
- const region = dragRef.current.region;
10665
- if (region === "prompt") {
10666
- const offset = promptOffsetAt(x, y);
10667
- dragRef.current.active = false;
10668
- promptMouseSelectionRef.current?.extendTo?.(offset, true);
10669
- return;
10670
- }
10671
- const span = dragRef.current.anchorSpan;
10672
- const releaseY = region === "status" ? clampToStatusBand(y) : clampToTranscriptViewport(y);
10673
- dragRef.current.active = false;
10674
- if (span) {
10675
- const rect = buildSpanRect(span, x, releaseY, region, dragRef.current.anchorScroll);
10676
- applySelectionRect(rect);
10677
- } else {
10678
- const anchor = region === "status" ? dragRef.current.anchor : selectionPointAtCurrentScroll(dragRef.current.anchor, dragRef.current.anchorScroll);
10679
- const rect = linearSelection(anchor, { x, y: releaseY });
10680
- const empty = rect.x1 === rect.x2 && rect.y1 === rect.y2;
10681
- if (empty) {
10682
- applySelectionRect(null);
10842
+ startEdgeAutoscroll(-1);
10683
10843
  } else {
10684
- applySelectionRect(rect);
10844
+ stopEdgeAutoscroll();
10685
10845
  }
10686
10846
  }
10687
- if (TRANSCRIPT_MEASURED_ROWS) setMeasuredRowsVersion((v) => (v + 1) % 1e6);
10847
+ } else if (!press && baseButton === 0 && dragRef.current.active) {
10848
+ finalizeActiveDrag(x, y);
10688
10849
  }
10689
10850
  }
10690
10851
  };
10691
10852
  inkInput.on("mouse", onMouse);
10692
10853
  return () => {
10693
10854
  inkInput.off("mouse", onMouse);
10855
+ stopEdgeAutoscroll();
10694
10856
  };
10695
10857
  }, [inkInput, isRawModeSupported, store, passthroughCtrlWheelZoom, rows, scrollTranscriptRows, queueScrollCoalesced, applySelectionRect, applySelectionRectThrottled, selectionPointAtCurrentScroll, buildSpanRect]);
10696
10858
  }
@@ -10742,26 +10904,53 @@ function useTranscriptScroll({
10742
10904
  const scroll = stitchHarvestScrollRef.current;
10743
10905
  for (const row of rows) {
10744
10906
  if (!row || typeof row.y !== "number") continue;
10745
- stitchBufferRef.current.set(row.y - scroll, typeof row.text === "string" ? row.text : "");
10907
+ stitchBufferRef.current.set(row.y - scroll, {
10908
+ text: typeof row.text === "string" ? row.text : "",
10909
+ sw: row.sw === true
10910
+ });
10746
10911
  }
10747
10912
  }, 0);
10748
10913
  }, [store]);
10914
+ const harvestStitchRowsNow = useCallback3((scroll) => {
10915
+ if (dragRef.current.region !== "transcript") return;
10916
+ const rows = store.getRenderSelectionRows?.();
10917
+ if (!Array.isArray(rows)) return;
10918
+ const s = Number(scroll) || 0;
10919
+ for (const row of rows) {
10920
+ if (!row || typeof row.y !== "number") continue;
10921
+ stitchBufferRef.current.set(row.y - s, {
10922
+ text: typeof row.text === "string" ? row.text : "",
10923
+ sw: row.sw === true
10924
+ });
10925
+ }
10926
+ }, [store]);
10749
10927
  const getStitchedSelectionText = useCallback3(() => {
10928
+ const empty = { text: "", complete: false };
10750
10929
  const buf = stitchBufferRef.current;
10751
- if (!buf.size) return "";
10752
- if (dragRef.current.region !== "transcript") return "";
10930
+ if (!buf.size) return empty;
10931
+ if (dragRef.current.region !== "transcript") return empty;
10753
10932
  const rect = dragRef.current.rect;
10754
- if (!rect) return "";
10933
+ if (!rect) return empty;
10755
10934
  const y1 = Number(rect.y1);
10756
10935
  const y2 = Number(rect.y2);
10757
- if (!Number.isFinite(y1) || !Number.isFinite(y2)) return "";
10936
+ if (!Number.isFinite(y1) || !Number.isFinite(y2)) return empty;
10758
10937
  const scroll = Number(scrollTargetRef.current) || 0;
10759
10938
  const lo = Math.min(y1, y2) - scroll;
10760
10939
  const hi = Math.max(y1, y2) - scroll;
10761
10940
  const keys = [...buf.keys()].filter((k) => k >= lo && k <= hi).sort((a, b) => a - b);
10762
- if (!keys.length) return "";
10763
- const text = keys.map((k) => buf.get(k)).filter((t) => t != null).join("\n");
10764
- return text.trim() ? text : "";
10941
+ if (!keys.length) return empty;
10942
+ const complete = keys.length === hi - lo + 1;
10943
+ const logical = [];
10944
+ for (const k of keys) {
10945
+ const entry = buf.get(k);
10946
+ if (entry == null) continue;
10947
+ const t = typeof entry === "string" ? entry : entry.text ?? "";
10948
+ const sw = typeof entry === "string" ? false : entry.sw === true;
10949
+ if (sw && logical.length > 0) logical[logical.length - 1] += t;
10950
+ else logical.push(t);
10951
+ }
10952
+ const text = logical.map((l) => l.replace(/\s+$/u, "")).join("\n");
10953
+ return text.trim() ? { text, complete } : empty;
10765
10954
  }, []);
10766
10955
  const stopSmoothScroll = useCallback3(() => {
10767
10956
  if (!scrollAnimationRef.current) return;
@@ -10856,6 +11045,25 @@ function useTranscriptScroll({
10856
11045
  if (nextRect) harvestStitchRowsSoon();
10857
11046
  return true;
10858
11047
  }, [store, rememberSelectionTextSoon, harvestStitchRowsSoon]);
11048
+ const cancelPendingSelectionPaint = useCallback3(() => {
11049
+ const state = selectionPaintRef.current;
11050
+ if (state.timer) {
11051
+ clearTimeout(state.timer);
11052
+ state.timer = null;
11053
+ }
11054
+ state.pending = null;
11055
+ }, []);
11056
+ const flushPendingSelectionPaint = useCallback3(() => {
11057
+ const state = selectionPaintRef.current;
11058
+ if (!state.timer && !state.pending) return;
11059
+ const pending = state.pending;
11060
+ if (state.timer) {
11061
+ clearTimeout(state.timer);
11062
+ state.timer = null;
11063
+ }
11064
+ state.pending = null;
11065
+ if (pending) paintSelectionRect(pending, { rememberText: false, immediate: true });
11066
+ }, [paintSelectionRect]);
10859
11067
  const applySelectionRect = useCallback3((rect) => {
10860
11068
  const clippedRect = withSelectionClip(rect);
10861
11069
  dragRef.current.rect = clippedRect || null;
@@ -10863,14 +11071,9 @@ function useTranscriptScroll({
10863
11071
  selectionTextRef.current = "";
10864
11072
  clearStitchBuffer();
10865
11073
  }
10866
- const state = selectionPaintRef.current;
10867
- if (state.timer) {
10868
- clearTimeout(state.timer);
10869
- state.timer = null;
10870
- state.pending = null;
10871
- }
11074
+ cancelPendingSelectionPaint();
10872
11075
  paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
10873
- }, [paintSelectionRect, withSelectionClip, clearStitchBuffer]);
11076
+ }, [paintSelectionRect, withSelectionClip, clearStitchBuffer, cancelPendingSelectionPaint]);
10874
11077
  const applySelectionRectThrottled = useCallback3((rect) => {
10875
11078
  const clippedRect = withSelectionClip(rect, { captureText: false });
10876
11079
  if (selectionRectsEqual(dragRef.current.rect, clippedRect)) return;
@@ -10880,11 +11083,7 @@ function useTranscriptScroll({
10880
11083
  const now = Date.now();
10881
11084
  const elapsed = now - state.t;
10882
11085
  if (elapsed >= SELECTION_PAINT_INTERVAL_MS) {
10883
- if (state.timer) {
10884
- clearTimeout(state.timer);
10885
- state.timer = null;
10886
- state.pending = null;
10887
- }
11086
+ cancelPendingSelectionPaint();
10888
11087
  paintSelectionRect(clippedRect, { rememberText: false });
10889
11088
  return;
10890
11089
  }
@@ -10899,7 +11098,7 @@ function useTranscriptScroll({
10899
11098
  }, Math.max(1, SELECTION_PAINT_INTERVAL_MS - elapsed));
10900
11099
  state.timer.unref?.();
10901
11100
  }
10902
- }, [paintSelectionRect, withSelectionClip]);
11101
+ }, [paintSelectionRect, withSelectionClip, cancelPendingSelectionPaint]);
10903
11102
  const selectionPointAtCurrentScroll = useCallback3((point, pointScroll = 0) => {
10904
11103
  if (!point) return null;
10905
11104
  return {
@@ -10929,14 +11128,14 @@ function useTranscriptScroll({
10929
11128
  mHi = { x: lr.x2, y: lr.y2 };
10930
11129
  } else {
10931
11130
  mLo = { x: 0, y };
10932
- mHi = { x, y };
11131
+ mHi = { x: Math.max(0, frameColumns - 1), y };
10933
11132
  }
10934
11133
  }
10935
11134
  const rect = (a, b) => ({ mode: "linear", x1: a.x, y1: a.y, x2: b.x, y2: b.y });
10936
11135
  if (comparePoints(mHi, spanLo) < 0) return rect(spanHi, mLo);
10937
11136
  if (comparePoints(mLo, spanHi) > 0) return rect(spanLo, mHi);
10938
11137
  return rect(spanLo, spanHi);
10939
- }, [store, selectionPointAtCurrentScroll]);
11138
+ }, [store, frameColumns, selectionPointAtCurrentScroll]);
10940
11139
  const transcriptViewportRows = useCallback3(() => {
10941
11140
  const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
10942
11141
  const bottom = Math.max(top, Number(transcriptViewportRef.current?.bottom) || top);
@@ -10977,6 +11176,10 @@ function useTranscriptScroll({
10977
11176
  const maxTarget = Math.max(0, Number(maxScrollRowsRef.current) || 0);
10978
11177
  const target = Math.max(0, Math.min(maxTarget, scrollTargetRef.current + deltaRows));
10979
11178
  const appliedDelta = target - scrollTargetRef.current;
11179
+ if (appliedDelta !== 0 && dragRef.current.region === "transcript" && dragRef.current.rect) {
11180
+ flushPendingSelectionPaint();
11181
+ harvestStitchRowsNow(Number(scrollTargetRef.current) || 0);
11182
+ }
10980
11183
  if (appliedDelta !== 0) cancelTranscriptFollow();
10981
11184
  scrollTargetRef.current = target;
10982
11185
  if (appliedDelta !== 0) {
@@ -11022,8 +11225,9 @@ function useTranscriptScroll({
11022
11225
  } else {
11023
11226
  rect = shiftSelectionRectY(dragRef.current.rect, appliedDelta);
11024
11227
  }
11025
- const clippedRect = withSelectionClip(rect);
11228
+ const clippedRect = dragRef.current.active ? withSelectionClip(rect, { captureText: false }) : withSelectionClip(rect);
11026
11229
  dragRef.current = { ...dragRef.current, rect: clippedRect };
11230
+ cancelPendingSelectionPaint();
11027
11231
  paintSelectionRect(clippedRect, { rememberText: false });
11028
11232
  }
11029
11233
  if (options.smooth) {
@@ -11033,7 +11237,7 @@ function useTranscriptScroll({
11033
11237
  stopSmoothScroll();
11034
11238
  scrollPositionRef.current = target;
11035
11239
  setScrollOffset(Math.round(target));
11036
- }, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect]);
11240
+ }, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect, harvestStitchRowsNow, cancelPendingSelectionPaint, flushPendingSelectionPaint]);
11037
11241
  const queueScrollCoalesced = useCallback3((deltaRows) => {
11038
11242
  const state = scrollCoalesceRef.current;
11039
11243
  state.pendingRows += deltaRows;
@@ -11189,6 +11393,7 @@ function useTranscriptWindow({
11189
11393
  }) {
11190
11394
  const transcriptTotalRowsRef = useRef8(0);
11191
11395
  const preservedScrollDeltaRef = useRef8(0);
11396
+ const committedMaxScrollRowsRef = useRef8(0);
11192
11397
  const incrementalRowIndexCacheRef = useRef8(null);
11193
11398
  const prevViewportGeomRef = useRef8({ contentHeight: 0, floatingPanelRows: 0 });
11194
11399
  const transcriptItemElsRef = useRef8(/* @__PURE__ */ new Map());
@@ -11358,7 +11563,31 @@ function useTranscriptWindow({
11358
11563
  rowIndex: transcriptRowIndex
11359
11564
  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig+scroll/viewport capture the relevant changes
11360
11565
  }), [transcriptStructureSig, renderScrollOffset, transcriptContentHeight, transcriptRowIndex]);
11361
- maxScrollRowsRef.current = transcriptWindow.maxScrollRows;
11566
+ const estimateMaxScrollRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
11567
+ let holdCommittedMax = false;
11568
+ if (TRANSCRIPT_MEASURED_ROWS && estimateMaxScrollRows > committedMaxScrollRowsRef.current) {
11569
+ const toolExpandedFlag = toolOutputExpanded ? 1 : 0;
11570
+ const mountedSlice = transcriptWindow.items || [];
11571
+ for (let i = 0; i < mountedSlice.length; i++) {
11572
+ const it = mountedSlice[i];
11573
+ if (!it || shouldSuppressFullyFailedToolItem(it)) continue;
11574
+ if (it.kind === "assistant" && it.streaming) {
11575
+ const idPrev = streamingMeasuredRowsById.get(it.id);
11576
+ if (!idPrev || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
11577
+ holdCommittedMax = true;
11578
+ break;
11579
+ }
11580
+ } else {
11581
+ const prev = transcriptMeasuredRowsCache.get(it);
11582
+ if (!prev || prev.columns !== frameColumns || prev.toolExpanded !== toolExpandedFlag || prev.variantKey !== transcriptItemVariantKey(it)) {
11583
+ holdCommittedMax = true;
11584
+ break;
11585
+ }
11586
+ }
11587
+ }
11588
+ }
11589
+ if (!holdCommittedMax) committedMaxScrollRowsRef.current = estimateMaxScrollRows;
11590
+ maxScrollRowsRef.current = committedMaxScrollRowsRef.current;
11362
11591
  transcriptGeomRef.current = {
11363
11592
  prefixRows: transcriptRowIndex?.prefixRows || null,
11364
11593
  totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
@@ -12576,6 +12805,7 @@ function createExtensionPickers({
12576
12805
  clean: clean3,
12577
12806
  copyToClipboard: copyToClipboard2,
12578
12807
  setPicker,
12808
+ getPicker,
12579
12809
  setProviderPrompt,
12580
12810
  setChannelPrompt,
12581
12811
  setHookPrompt,
@@ -12583,6 +12813,8 @@ function createExtensionPickers({
12583
12813
  getDisabledSkills,
12584
12814
  setDisabledSkills
12585
12815
  }) {
12816
+ let mcpEpoch = 0;
12817
+ let mcpActive = false;
12586
12818
  const mcpStatus = () => {
12587
12819
  let status;
12588
12820
  try {
@@ -12597,6 +12829,7 @@ function createExtensionPickers({
12597
12829
  const status = mcpStatus();
12598
12830
  if (!status) return;
12599
12831
  const servers = status.servers || [];
12832
+ const optimistic = options?.optimistic || null;
12600
12833
  const items = [];
12601
12834
  if (servers.length === 0) {
12602
12835
  items.push({
@@ -12607,13 +12840,14 @@ function createExtensionPickers({
12607
12840
  });
12608
12841
  }
12609
12842
  for (const server of servers) {
12610
- const enabled = server.enabled !== false;
12843
+ const pending = optimistic && optimistic.name === server.name;
12844
+ const enabled = pending ? optimistic.enabled : server.enabled !== false;
12611
12845
  items.push({
12612
12846
  value: `server:${server.name}`,
12613
12847
  label: server.name,
12614
12848
  marker: enabled ? "\u25CF" : "\u25CB",
12615
12849
  markerColor: enabled ? theme2.success : theme2.inactive,
12616
- description: `${server.status || "unknown"} \xB7 ${server.transport || "unknown"} \xB7 ${server.toolCount || 0} tools${server.error ? ` \xB7 ${server.error}` : ""}`,
12850
+ description: pending ? `${optimistic.enabled ? "enabling" : "disabling"}\u2026 \xB7 ${server.transport || "unknown"}` : `${server.status || "unknown"} \xB7 ${server.transport || "unknown"} \xB7 ${server.toolCount || 0} tools${server.error ? ` \xB7 ${server.error}` : ""}`,
12617
12851
  _action: "server",
12618
12852
  _server: server,
12619
12853
  _enabled: enabled
@@ -12625,9 +12859,24 @@ function createExtensionPickers({
12625
12859
  setSettingsPrompt(null);
12626
12860
  const toggleServer = (item) => {
12627
12861
  if (item._action !== "server" || !item._server?.name) return;
12628
- void store.setMcpServerEnabled?.(item._server.name, !item._enabled).then(() => openMcpServersPicker({ highlightValue: `server:${item._server.name}` })).catch((e) => store.pushNotice(`mcp toggle failed: ${e?.message || e}`, "error"));
12862
+ const name = item._server.name;
12863
+ const target = !item._enabled;
12864
+ const highlightValue = `server:${name}`;
12865
+ const token = ++mcpEpoch;
12866
+ const settle = (fn) => {
12867
+ if (token !== mcpEpoch || !mcpActive || getPicker?.()?._kind !== "mcp-servers") return;
12868
+ fn();
12869
+ };
12870
+ openMcpServersPicker({ highlightValue, optimistic: { name, enabled: target } });
12871
+ Promise.resolve(store.setMcpServerEnabled?.(name, target)).then(() => {
12872
+ settle(() => openMcpServersPicker({ highlightValue }));
12873
+ }).catch((e) => {
12874
+ store.pushNotice(`mcp toggle failed: ${e?.message || e}`, "error");
12875
+ settle(() => openMcpServersPicker({ highlightValue }));
12876
+ });
12629
12877
  };
12630
12878
  setPicker({
12879
+ _kind: "mcp-servers",
12631
12880
  title: "MCP servers",
12632
12881
  description: "Enable or disable configured MCP servers.",
12633
12882
  initialIndex: Math.max(0, items.findIndex((entry) => entry.value === options?.highlightValue)),
@@ -12636,9 +12885,12 @@ function createExtensionPickers({
12636
12885
  onLeft: (item) => toggleServer(item),
12637
12886
  onRight: (item) => toggleServer(item),
12638
12887
  onCancel: () => {
12888
+ mcpActive = false;
12889
+ mcpEpoch++;
12639
12890
  setPicker(null);
12640
12891
  }
12641
12892
  });
12893
+ mcpActive = true;
12642
12894
  };
12643
12895
  const openMcpPicker = () => {
12644
12896
  openMcpServersPicker();
@@ -12695,9 +12947,14 @@ function createExtensionPickers({
12695
12947
  });
12696
12948
  };
12697
12949
  const openSkillsPicker = (options = {}) => {
12698
- const status = skillsStatus();
12699
- if (!status) return;
12700
- const skills = status.skills || [];
12950
+ let skills;
12951
+ if (Array.isArray(options.skills)) {
12952
+ skills = options.skills;
12953
+ } else {
12954
+ const status = skillsStatus();
12955
+ if (!status) return;
12956
+ skills = status.skills || [];
12957
+ }
12701
12958
  const disabledSet = options.disabledOverride instanceof Set ? options.disabledOverride : getDisabledSkills();
12702
12959
  const items = [];
12703
12960
  if (skills.length === 0) {
@@ -12736,9 +12993,10 @@ function createExtensionPickers({
12736
12993
  `skill ${item._enabled ? "disabled" : "enabled"}: ${name} (prompt updates next session /clear)`,
12737
12994
  "info"
12738
12995
  );
12739
- openSkillsPicker({ highlightValue: name, disabledOverride: next });
12996
+ openSkillsPicker({ highlightValue: name, disabledOverride: next, skills });
12740
12997
  };
12741
12998
  setPicker({
12999
+ _kind: "skills",
12742
13000
  title: "Skills",
12743
13001
  description: "Enable or disable project skills.",
12744
13002
  initialIndex: Math.max(0, items.findIndex((entry) => entry.value === options.highlightValue)),
@@ -13021,6 +13279,7 @@ ${p.description}` : ""
13021
13279
  }
13022
13280
  };
13023
13281
  setPicker({
13282
+ _kind: "hooks",
13024
13283
  title: "Hooks",
13025
13284
  description: "Before-tool hook rules; Enter toggles a rule.",
13026
13285
  items,
@@ -13249,8 +13508,6 @@ function createMaintenancePickers({
13249
13508
  const current = readCurrent();
13250
13509
  const enabled = current?.enabled !== false;
13251
13510
  const idleMs = Number(current?.idleMs || HOUR_MS);
13252
- const custom = current?.custom === true;
13253
- const providerDefault = Number(current?.providerDefault || HOUR_MS);
13254
13511
  const cacheTtlLabel = !enabled || idleMs >= HOUR_MS ? "1h" : "5m";
13255
13512
  const items = [
13256
13513
  {
@@ -13263,9 +13520,6 @@ function createMaintenancePickers({
13263
13520
  {
13264
13521
  value: "advanced",
13265
13522
  label: "Advanced",
13266
- marker: !custom ? "\u2713" : "",
13267
- markerColor: theme2.success,
13268
- meta: `${current?.provider || "current"} \xB7 ${formatDuration2(providerDefault)}`,
13269
13523
  description: "Edit provider-paired default idle windows as text.",
13270
13524
  _action: "advanced"
13271
13525
  }
@@ -13748,6 +14002,18 @@ function sleepSync(ms) {
13748
14002
  }
13749
14003
  }
13750
14004
  }
14005
+ function _describeLockHolder(lockPath) {
14006
+ try {
14007
+ const st = statSync2(lockPath);
14008
+ const owner = _readLockOwner(lockPath);
14009
+ const ageMs = Math.max(0, Math.round(Date.now() - st.mtimeMs));
14010
+ const live = owner.pid === null ? "unknown" : _ownerIsLive(owner) ? "live" : "dead";
14011
+ const token = owner.token === null ? "?" : String(owner.token).slice(0, 8);
14012
+ return `holder pid=${owner.pid ?? "?"} token=${token} age=${ageMs}ms ${live}`;
14013
+ } catch {
14014
+ return "holder unknown (lock file unreadable/absent)";
14015
+ }
14016
+ }
13751
14017
  function renameWithRetrySync(src, dst, opts = {}) {
13752
14018
  const backoffs = Array.isArray(opts.backoffs) && opts.backoffs.length > 0 ? opts.backoffs : DEFAULT_BACKOFFS_MS;
13753
14019
  let lastErr = null;
@@ -13779,7 +14045,7 @@ function withFileLockSync(lockPath, fn, opts = {}) {
13779
14045
  lastErr = err;
13780
14046
  if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
13781
14047
  if (timeoutMs <= 0) {
13782
- const contErr = new Error(`atomic lock contended (try-once): ${lockPath}`);
14048
+ const contErr = new Error(`atomic lock contended (try-once): ${lockPath} [${_describeLockHolder(lockPath)}]`);
13783
14049
  contErr.code = "ELOCKCONTENDED";
13784
14050
  contErr.cause = err;
13785
14051
  throw contErr;
@@ -13814,7 +14080,7 @@ function withFileLockSync(lockPath, fn, opts = {}) {
13814
14080
  }
13815
14081
  }
13816
14082
  }
13817
- const timeoutErr = new Error(`atomic lock timeout after ${timeoutMs}ms: ${lockPath}`);
14083
+ const timeoutErr = new Error(`atomic lock timeout after ${timeoutMs}ms: ${lockPath} [${_describeLockHolder(lockPath)}]`);
13818
14084
  timeoutErr.code = "ELOCKTIMEOUT";
13819
14085
  timeoutErr.cause = lastErr;
13820
14086
  throw timeoutErr;
@@ -13927,8 +14193,21 @@ function _reclaimGuardMatchesToken(guardPath, token) {
13927
14193
  return false;
13928
14194
  }
13929
14195
  }
13930
- function _unlinkReclaimGuardIfPidMatches(guardPath, pid) {
13931
- if (_readLockOwnerPid(guardPath) !== pid) return false;
14196
+ function _readGuardContent(guardPath) {
14197
+ try {
14198
+ return readFileSync2(guardPath, "utf8");
14199
+ } catch {
14200
+ return null;
14201
+ }
14202
+ }
14203
+ function _parseStampPid(raw) {
14204
+ if (raw === null) return null;
14205
+ const tok = String(raw).trim().split(/\s+/)[0];
14206
+ const pid = Number.parseInt(tok, 10);
14207
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
14208
+ }
14209
+ function _unlinkReclaimGuardIfContentMatches(guardPath, content) {
14210
+ if (_readGuardContent(guardPath) !== content) return false;
13932
14211
  try {
13933
14212
  unlinkSync2(guardPath);
13934
14213
  return true;
@@ -13946,7 +14225,8 @@ function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
13946
14225
  }
13947
14226
  }
13948
14227
  function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13949
- const guardPid = _readLockOwnerPid(guardPath);
14228
+ const guardContent = _readGuardContent(guardPath);
14229
+ const guardPid = _parseStampPid(guardContent);
13950
14230
  if (guardPid !== null) {
13951
14231
  let guardStale = false;
13952
14232
  try {
@@ -13954,7 +14234,7 @@ function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13954
14234
  } catch {
13955
14235
  }
13956
14236
  if (_pidIsDead(guardPid) || guardStale) {
13957
- _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
14237
+ _unlinkReclaimGuardIfContentMatches(guardPath, guardContent);
13958
14238
  }
13959
14239
  return;
13960
14240
  }
@@ -13992,8 +14272,8 @@ function _releaseReclaimGuard(reclaim) {
13992
14272
  }
13993
14273
  }
13994
14274
  function _asyncSleep(ms) {
13995
- return new Promise((resolve6) => {
13996
- setTimeout(resolve6, Math.max(1, Number(ms) || 1));
14275
+ return new Promise((resolve5) => {
14276
+ setTimeout(resolve5, Math.max(1, Number(ms) || 1));
13997
14277
  });
13998
14278
  }
13999
14279
  async function withFileLock(lockPath, fn, opts = {}) {
@@ -14011,7 +14291,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14011
14291
  lastErr = err;
14012
14292
  if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
14013
14293
  if (timeoutMs <= 0) {
14014
- const contErr = new Error(`atomic lock contended (try-once): ${lockPath}`);
14294
+ const contErr = new Error(`atomic lock contended (try-once): ${lockPath} [${_describeLockHolder(lockPath)}]`);
14015
14295
  contErr.code = "ELOCKCONTENDED";
14016
14296
  contErr.cause = err;
14017
14297
  throw contErr;
@@ -14046,7 +14326,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14046
14326
  }
14047
14327
  }
14048
14328
  }
14049
- const timeoutErr = new Error(`atomic lock timeout after ${timeoutMs}ms: ${lockPath}`);
14329
+ const timeoutErr = new Error(`atomic lock timeout after ${timeoutMs}ms: ${lockPath} [${_describeLockHolder(lockPath)}]`);
14050
14330
  timeoutErr.code = "ELOCKTIMEOUT";
14051
14331
  timeoutErr.cause = lastErr;
14052
14332
  throw timeoutErr;
@@ -14196,21 +14476,6 @@ function writeFileAtomicSync(filePath, data, opts = {}) {
14196
14476
  function writeJsonAtomicSync(filePath, value, opts = {}) {
14197
14477
  return writeFileAtomicSync(filePath, JSON.stringify(value, null, opts.compact ? 0 : 2) + "\n", opts);
14198
14478
  }
14199
- function updateJsonAtomicSync(filePath, mutator, opts = {}) {
14200
- const { lock: _lock, ...writeOpts } = opts;
14201
- return withFileLockSync(`${filePath}.lock`, () => {
14202
- let cur = null;
14203
- try {
14204
- cur = JSON.parse(readFileSync2(filePath, "utf8"));
14205
- } catch {
14206
- cur = null;
14207
- }
14208
- const next = mutator(cur);
14209
- if (next === void 0) return cur;
14210
- writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
14211
- return next;
14212
- }, opts);
14213
- }
14214
14479
  async function updateJsonAtomic(filePath, mutator, opts = {}) {
14215
14480
  const { lock: _lock, ...writeOpts } = opts;
14216
14481
  return withFileLock(`${filePath}.lock`, () => {
@@ -15160,7 +15425,7 @@ function createModelPicker({
15160
15425
  items: [],
15161
15426
  onCancel: cancelModelPicker
15162
15427
  });
15163
- await new Promise((resolve6) => setTimeout(resolve6, 0));
15428
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
15164
15429
  try {
15165
15430
  if (options.refreshModels !== true && options.cacheRef !== "search") {
15166
15431
  refreshModelsPromise = Promise.resolve(loadModels({ force: false }));
@@ -15486,7 +15751,7 @@ function createProviderSetupPicker({
15486
15751
  }
15487
15752
  });
15488
15753
  try {
15489
- await new Promise((resolve6) => setTimeout(resolve6, 0));
15754
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
15490
15755
  setup = await store.getProviderSetup();
15491
15756
  } catch (e) {
15492
15757
  store.pushNotice(`providers failed: ${e?.message || e}`, "error");
@@ -16872,10 +17137,6 @@ function createSlashDispatch({
16872
17137
  return true;
16873
17138
  }
16874
17139
  case "search":
16875
- if (state.busy) {
16876
- store.pushNotice("wait for the current turn to finish before /search", "warn");
16877
- return false;
16878
- }
16879
17140
  if (arg) store.pushNotice("/search sets the search provider/model; the search tool uses that model when called.", "warn");
16880
17141
  openSearchPicker();
16881
17142
  return true;
@@ -17276,57 +17537,64 @@ function usePromptHandlers({
17276
17537
  const handlePromptPaste = useCallback5((text, meta = {}) => {
17277
17538
  const source = String(meta?.source || "paste");
17278
17539
  const value = String(text ?? "");
17279
- if (source === "clipboard-image-shortcut" && !value) {
17280
- return readClipboardImageAttachment().then((image) => {
17281
- if (!image) {
17282
- showPromptHint("no image found on clipboard", "plain");
17283
- return false;
17540
+ const processText = (raw, returnRaw = false) => {
17541
+ const chunks = splitPastedImagePathCandidates(raw);
17542
+ const hasImagePath = chunks.some((chunk) => chunk.imagePath);
17543
+ if (!hasImagePath) {
17544
+ if (shouldFoldPastedText(raw)) return registerPastedText(raw);
17545
+ return returnRaw ? raw : void 0;
17546
+ }
17547
+ return Promise.all(chunks.map(async (chunk) => {
17548
+ if (!chunk.imagePath) return chunk.text;
17549
+ try {
17550
+ const image = await readImageAttachmentFromPath(chunk.text, state.cwd || process.cwd());
17551
+ if (!image) return chunk.text;
17552
+ const ref = registerPastedImage(image);
17553
+ showPromptHint(`attached ${image.filename || "image"}`, "plain");
17554
+ return ref;
17555
+ } catch (e) {
17556
+ showPromptHint(`image attach failed: ${e?.message || e}`, "warn");
17557
+ return chunk.text;
17284
17558
  }
17285
- const ref = registerPastedImage(image);
17286
- showPromptHint(`attached ${image.filename || "clipboard image"}`, "plain");
17287
- return ref;
17559
+ })).then((parts) => {
17560
+ let out = "";
17561
+ let run = "";
17562
+ const flushRun = () => {
17563
+ if (!run) return;
17564
+ out += shouldFoldPastedText(run) ? registerPastedText(run) : run;
17565
+ run = "";
17566
+ };
17567
+ for (let i = 0; i < chunks.length; i += 1) {
17568
+ if (chunks[i].imagePath) {
17569
+ flushRun();
17570
+ out += parts[i];
17571
+ } else {
17572
+ run += parts[i];
17573
+ }
17574
+ }
17575
+ flushRun();
17576
+ return out;
17577
+ });
17578
+ };
17579
+ if (source === "clipboard-shortcut" && !value) {
17580
+ return readClipboardText().then((clip) => {
17581
+ const normalized = String(clip ?? "").replace(/\r\n?/g, "\n");
17582
+ if (normalized) return processText(normalized, true);
17583
+ return readClipboardImageAttachment().then((image) => {
17584
+ if (!image) {
17585
+ showPromptHint("no text or image found on clipboard", "plain");
17586
+ return false;
17587
+ }
17588
+ const ref = registerPastedImage(image);
17589
+ showPromptHint(`attached ${image.filename || "clipboard image"}`, "plain");
17590
+ return ref;
17591
+ });
17288
17592
  }).catch((e) => {
17289
- showPromptHint(`image paste failed: ${e?.message || e}`, "warn");
17593
+ showPromptHint(`paste failed: ${e?.message || e}`, "warn");
17290
17594
  return false;
17291
17595
  });
17292
17596
  }
17293
- const chunks = splitPastedImagePathCandidates(value);
17294
- const hasImagePath = chunks.some((chunk) => chunk.imagePath);
17295
- if (!hasImagePath) {
17296
- if (shouldFoldPastedText(value)) return registerPastedText(value);
17297
- return void 0;
17298
- }
17299
- return Promise.all(chunks.map(async (chunk) => {
17300
- if (!chunk.imagePath) return chunk.text;
17301
- try {
17302
- const image = await readImageAttachmentFromPath(chunk.text, state.cwd || process.cwd());
17303
- if (!image) return chunk.text;
17304
- const ref = registerPastedImage(image);
17305
- showPromptHint(`attached ${image.filename || "image"}`, "plain");
17306
- return ref;
17307
- } catch (e) {
17308
- showPromptHint(`image attach failed: ${e?.message || e}`, "warn");
17309
- return chunk.text;
17310
- }
17311
- })).then((parts) => {
17312
- let out = "";
17313
- let run = "";
17314
- const flushRun = () => {
17315
- if (!run) return;
17316
- out += shouldFoldPastedText(run) ? registerPastedText(run) : run;
17317
- run = "";
17318
- };
17319
- for (let i = 0; i < chunks.length; i += 1) {
17320
- if (chunks[i].imagePath) {
17321
- flushRun();
17322
- out += parts[i];
17323
- } else {
17324
- run += parts[i];
17325
- }
17326
- }
17327
- flushRun();
17328
- return out;
17329
- });
17597
+ return processText(value);
17330
17598
  }, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
17331
17599
  const handlePromptHistoryNavigate = useCallback5((direction, currentText = "", meta = {}) => {
17332
17600
  const currentValue = String(currentText || "");
@@ -17675,7 +17943,7 @@ var AssistantMessage = React14.memo(function AssistantMessage2({
17675
17943
  });
17676
17944
  var UserMessage = React14.memo(function UserMessage2({ text, attached = false, columns, themeEpoch = 0 }) {
17677
17945
  const bandColumns = Math.max(1, columns - 1);
17678
- return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", width: bandColumns, marginTop: attached ? 0 : 1, backgroundColor: theme.userMessageBackground, paddingLeft: 2, paddingRight: 1, children: /* @__PURE__ */ jsx14(Text14, { color: theme.text, wrap: "wrap", children: text }) });
17946
+ return /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", width: bandColumns, marginTop: attached ? 0 : 1, backgroundColor: theme.userMessageBackground, paddingLeft: 2, paddingRight: 1, children: /* @__PURE__ */ jsx14(Text14, { color: theme.mixdogIvory, wrap: "wrap", children: text }) });
17679
17947
  });
17680
17948
  function NoticeMessage({ text, tone, columns = 80 }) {
17681
17949
  const accentColor = tone === "error" ? theme.error : tone === "warn" ? theme.warning : theme.inactive;
@@ -18364,13 +18632,13 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18364
18632
  else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
18365
18633
  else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : "") || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
18366
18634
  labelText = safeInlineText(labelText);
18367
- const toolSearchSummary = !pending && normalizedName === "tool_search" && hasResult ? toolSearchLoadedSummary(displayedResultText) : "";
18635
+ const toolSearchSummary = !pending && normalizedName === "load_tool" && hasResult ? toolSearchLoadedSummary(displayedResultText) : "";
18368
18636
  const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse ? "" : toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary));
18369
18637
  const summaryIsHeaderCount = rawSummaryText && /^\d+\s+\S+$/.test(rawSummaryText) && labelText.endsWith(rawSummaryText);
18370
18638
  const summaryText = summaryIsHeaderCount ? "" : rawSummaryText;
18371
18639
  const agentHasExpandableBody = isAgentSurfaceCard && !pending && hasResult && (isAgentResponse || totalLines > 1 || firstResultLineClipped);
18372
18640
  const shellHasExpandableBody = isShellSurface && !pending && hasDisplayResult && hasDisplayBody && (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
18373
- const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : isAgentSurfaceCard ? agentHasExpandableBody : hasHiddenDetail || backgroundMetadataExpandable) && normalizedName !== "tool_search";
18641
+ const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : isAgentSurfaceCard ? agentHasExpandableBody : hasHiddenDetail || backgroundMetadataExpandable) && normalizedName !== "load_tool";
18374
18642
  const expandHintColor = theme.subtle;
18375
18643
  const gutter = 2;
18376
18644
  const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? "collapse" : "expand"}` : "";
@@ -18602,7 +18870,9 @@ var PANEL_LAYOUT_SIG = {
18602
18870
  TEXT: 5,
18603
18871
  // Prompt-wrap/meta row counts (trailing churn tokens, see token order note
18604
18872
  // below). PROMPT_META is the 2-row live-spinner band slot.
18605
- PROMPT_META: 9
18873
+ PROMPT_META: 9,
18874
+ // Queued steering band rows (full wrapped height, see queuedBandRows).
18875
+ QUEUED: 10
18606
18876
  };
18607
18877
  var PROJECT_TEXT_ENTRY_KINDS = /* @__PURE__ */ new Set(["project-new", "project-create-confirm", "project-rename"]);
18608
18878
  var CORE_MULTILINE_TEXT_ENTRY_KINDS = /* @__PURE__ */ new Set(["core-add", "core-edit"]);
@@ -18664,7 +18934,9 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18664
18934
  const projectPickerRef = useRef9(null);
18665
18935
  const buildProjectPickerState = (opts) => projectPickerRef.current.buildProjectPickerState(opts);
18666
18936
  const [picker, setPickerState] = useState8(null);
18937
+ const livePickerRef = useRef9(null);
18667
18938
  const setPicker = useCallback6((next) => {
18939
+ livePickerRef.current = typeof next === "function" ? next(livePickerRef.current) : next;
18668
18940
  setPickerState((prev) => {
18669
18941
  const resolved = typeof next === "function" ? next(prev) : next;
18670
18942
  if (resolved && typeof resolved === "object" && pickerOpenedFromEnterRef.current) {
@@ -18675,9 +18947,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18675
18947
  }
18676
18948
  return resolved.indexMode ? resolved : { ...resolved, indexMode: "always" };
18677
18949
  }
18950
+ if (resolved && typeof resolved === "object" && !resolved.indexMode && prev && typeof prev === "object" && prev.indexMode && prev._kind && prev._kind === resolved._kind) {
18951
+ return { ...resolved, indexMode: prev.indexMode };
18952
+ }
18678
18953
  return resolved;
18679
18954
  });
18680
18955
  }, []);
18956
+ livePickerRef.current = picker;
18681
18957
  const [contextPanel, setContextPanel] = useState8(null);
18682
18958
  const [usagePanel, setUsagePanel] = useState8(null);
18683
18959
  const usageRequestRef = useRef9(0);
@@ -18719,7 +18995,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18719
18995
  } catch {
18720
18996
  }
18721
18997
  if (!onboardingOwnsScreen && state.items.length === 0) {
18722
- setPickerState(projectPicker.buildProjectPickerState({ initialEntry: true }));
18998
+ setPicker(projectPicker.buildProjectPickerState({ initialEntry: true }));
18723
18999
  }
18724
19000
  }
18725
19001
  const {
@@ -18790,6 +19066,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18790
19066
  clean,
18791
19067
  copyToClipboard,
18792
19068
  setPicker,
19069
+ getPicker: () => livePickerRef.current,
18793
19070
  setProviderPrompt,
18794
19071
  setChannelPrompt,
18795
19072
  setHookPrompt,
@@ -19292,13 +19569,16 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
19292
19569
  const renderText = store.getRenderSelectionText?.();
19293
19570
  const remembered = selectionTextRef.current || "";
19294
19571
  let text = renderText == null ? remembered : remembered.length > renderText.length ? remembered : renderText;
19295
- const stitched = getStitchedSelectionText?.() || "";
19296
- if (stitched.length > text.length) text = stitched;
19572
+ const stitched = getStitchedSelectionText?.() || { text: "", complete: false };
19573
+ if (stitched.complete && stitched.text.length > text.length) text = stitched.text;
19297
19574
  if ((!text || !text.trim()) && attempt < 4) {
19298
19575
  setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
19299
19576
  return;
19300
19577
  }
19301
- if (!text || !text.trim()) return;
19578
+ if (!text || !text.trim()) {
19579
+ showSelectionCopyHint("nothing to copy \xB7 select text first", "error");
19580
+ return;
19581
+ }
19302
19582
  selectionTextRef.current = text;
19303
19583
  copyToClipboard(text).then(() => {
19304
19584
  const lines = text.split("\n").length;
@@ -19374,8 +19654,8 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
19374
19654
  let timer = null;
19375
19655
  Promise.race([
19376
19656
  Promise.resolve(store.dispose?.("cli-react-exit", { detach: true })),
19377
- new Promise((resolve6) => {
19378
- timer = setTimeout(resolve6, 350);
19657
+ new Promise((resolve5) => {
19658
+ timer = setTimeout(resolve5, 350);
19379
19659
  })
19380
19660
  ]).finally(() => {
19381
19661
  if (timer) clearTimeout(timer);
@@ -20481,7 +20761,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20481
20761
  const promptMetaRows = promptMetaVisible ? 2 : 0;
20482
20762
  const overlayHintRequested = !inputBoxHidden && !hasFloatingPanel && !liveSpinner && !!inputHint && !queuedVisible;
20483
20763
  const overlayHintRows = 0;
20484
- const queuedRows = queuedVisible ? state.queued.length : 0;
20764
+ const queuedFullRows = queuedVisible ? state.queued.reduce(
20765
+ (sum, item) => sum + queuedBandRows(String(item.displayText || item.text || ""), Math.max(1, frameColumns - 4)),
20766
+ 0
20767
+ ) : 0;
20768
+ const queuedRowBudget = Math.max(3, Math.floor(resizeState.rows / 3));
20769
+ const queuedCompact = queuedFullRows > queuedRowBudget;
20770
+ const queuedRows = queuedVisible ? queuedCompact ? state.queued.length : queuedFullRows : 0;
20485
20771
  const INPUT_BOX_ROWS = promptBoxRows + promptMetaRows + overlayHintRows;
20486
20772
  const baseReserve = WELCOME_ROWS + SCROLL_HINT_ROWS + LIVE_STATUS_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows;
20487
20773
  const maxFloatingPanelRows = Math.max(0, resizeState.rows - baseReserve - 1);
@@ -20516,10 +20802,14 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20516
20802
  const nextMetaRows = Number(String(panelLayoutSignature).split("|")[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
20517
20803
  const doneTailAppendedThisCommit = latestDoneAtTail && (latestTranscriptItem?.id ?? null) !== panelTransition.tailId;
20518
20804
  const spinnerMetaCollapseRows = doneTailAppendedThisCommit ? Math.max(0, prevMetaRows - nextMetaRows) : 0;
20805
+ const prevQueuedSigRows = Number(String(panelTransition.signature).split("|")[PANEL_LAYOUT_SIG.QUEUED]) || 0;
20806
+ const nextQueuedSigRows = Number(String(panelLayoutSignature).split("|")[PANEL_LAYOUT_SIG.QUEUED]) || 0;
20807
+ const userTailAppendedThisCommit = latestTranscriptItem?.kind === "user" && (latestTranscriptItem?.id ?? null) !== panelTransition.tailId;
20808
+ const queuedPromoteCollapseRows = userTailAppendedThisCommit ? Math.max(0, prevQueuedSigRows - nextQueuedSigRows) : 0;
20519
20809
  const instantPanelClose = panelShrinkRows > 0 && (promptRowsOnlyChange || isInstantPanelCloseTransition(panelTransition.signature, panelLayoutSignature, initialProjectEntryClose));
20520
20810
  const slashOpenOnEmptyTranscript = initialProjectEntryClose && !panelSignatureFlags(panelTransition.signature).slash && panelSignatureFlags(panelLayoutSignature).slash;
20521
20811
  if (instantPanelClose) {
20522
- panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows);
20812
+ panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows - queuedPromoteCollapseRows);
20523
20813
  panelTransition.clearRows = 0;
20524
20814
  panelTransition.guardRows = 0;
20525
20815
  panelTransition.epoch = panelTransitionEpoch;
@@ -20548,8 +20838,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20548
20838
  const viewportHeight = Math.max(1, resizeState.rows - bottomReserve);
20549
20839
  const guardCapacityRows = Math.max(0, viewportHeight - 1);
20550
20840
  const baseGuardRows = guardCapacityRows > 0 ? 1 : 0;
20551
- const scrollGuardNearBottom = followingRef.current || scrollTargetRef.current <= baseGuardRows;
20552
- const scrollGuardRows = !scrollGuardNearBottom && scrollOffset > 0 && guardCapacityRows > baseGuardRows ? 1 : 0;
20841
+ const scrollGuardRows = 0;
20553
20842
  const transcriptGuardRows = Math.min(guardCapacityRows, baseGuardRows + panelTransitionGuardRows + scrollGuardRows);
20554
20843
  const welcomePromptHintText = conditionalWelcomePromptHint || welcomePromptHintRef.current || "";
20555
20844
  const welcomePromptHintVisible = Boolean(
@@ -20557,13 +20846,14 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20557
20846
  );
20558
20847
  const welcomePromptHintRows = welcomePromptHintVisible && viewportHeight - transcriptGuardRows >= 2 ? 1 : 0;
20559
20848
  welcomePromptHintVisibleRef.current = welcomePromptHintRows > 0;
20849
+ const overlayHintBandRows = overlayHintRequested && state.items.length === 0 && viewportHeight - transcriptGuardRows - welcomePromptHintRows >= 2 ? 1 : 0;
20560
20850
  const panelCloseMaskRows = Math.min(
20561
20851
  panelCloseInkMaskRows,
20562
- Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - 1)
20852
+ Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - overlayHintBandRows - 1)
20563
20853
  );
20564
20854
  const transcriptContentHeight = Math.max(
20565
20855
  1,
20566
- viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows
20856
+ viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows - overlayHintBandRows
20567
20857
  );
20568
20858
  const transcriptBottomSlackRows = Math.max(0, baseGuardRows);
20569
20859
  transcriptBottomSlackRowsRef.current = transcriptBottomSlackRows;
@@ -20785,9 +21075,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20785
21075
  backgroundColor: surfaceBackground()
20786
21076
  }
20787
21077
  ) : null,
21078
+ overlayHintBandRows > 0 ? /* @__PURE__ */ jsxs16(Box18, { height: 1, flexShrink: 0, backgroundColor: surfaceBackground(), flexDirection: "row", width: "100%", overflow: "hidden", children: [
21079
+ /* @__PURE__ */ jsx20(Box18, { flexGrow: 1, flexShrink: 1, overflow: "hidden" }),
21080
+ /* @__PURE__ */ jsx20(Box18, { flexShrink: 0, width: guardHintWidth || 1, marginLeft: 1, marginRight: 1, justifyContent: "flex-end", overflow: "hidden", children: /* @__PURE__ */ jsx20(Text20, { color: promptStatusColor(inputHintTone), wrap: "truncate", children: inputHint }) })
21081
+ ] }) : null,
20788
21082
  transcriptGuardRows > 0 ? /* @__PURE__ */ jsxs16(Box18, { height: transcriptGuardRows, flexShrink: 0, backgroundColor: surfaceBackground(), flexDirection: "row", width: "100%", overflow: "hidden", children: [
20789
21083
  /* @__PURE__ */ jsx20(Box18, { flexGrow: 1, flexShrink: 1, overflow: "hidden" }),
20790
- overlayHintFallbackRow ? /* @__PURE__ */ jsx20(Box18, { flexShrink: 0, width: guardHintWidth || 1, marginLeft: 1, marginRight: 1, justifyContent: "flex-end", overflow: "hidden", children: /* @__PURE__ */ jsx20(Text20, { color: promptStatusColor(inputHintTone), wrap: "truncate", children: inputHint }) }) : null
21084
+ overlayHintFallbackRow && overlayHintBandRows === 0 ? /* @__PURE__ */ jsx20(Box18, { flexShrink: 0, width: guardHintWidth || 1, marginLeft: 1, marginRight: 1, justifyContent: "flex-end", overflow: "hidden", children: /* @__PURE__ */ jsx20(Text20, { color: promptStatusColor(inputHintTone), wrap: "truncate", children: inputHint }) }) : null
20791
21085
  ] }) : null
20792
21086
  ]
20793
21087
  }
@@ -20986,7 +21280,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20986
21280
  ),
20987
21281
  /* @__PURE__ */ jsx20(Box18, { height: 1, width: "100%", backgroundColor: surfaceBackground() })
20988
21282
  ] }) : null,
20989
- queuedVisible ? /* @__PURE__ */ jsx20(QueuedCommands, { queued: state.queued, columns: frameColumns }) : null,
21283
+ queuedVisible ? /* @__PURE__ */ jsx20(QueuedCommands, { queued: state.queued, columns: frameColumns, compact: queuedCompact }) : null,
20990
21284
  /* @__PURE__ */ jsx20(
20991
21285
  Box18,
20992
21286
  {
@@ -21174,6 +21468,33 @@ function modelVisibleToolCompletionMessage(text, meta = {}) {
21174
21468
  ].join("\n");
21175
21469
  }
21176
21470
 
21471
+ // src/session-runtime/session-text.mjs
21472
+ var LATE_TOOL_ANNOUNCEMENT_SENTINEL = "connected after this session started";
21473
+ function isLateToolAnnouncement(text) {
21474
+ const value = String(text || "");
21475
+ return value.includes(LATE_TOOL_ANNOUNCEMENT_SENTINEL) && /<available-deferred-tools>/i.test(value);
21476
+ }
21477
+ function summarizeLateToolAnnouncement(text) {
21478
+ const value = String(text || "");
21479
+ if (!isLateToolAnnouncement(value)) return "";
21480
+ const block = value.match(/<available-deferred-tools>([\s\S]*?)<\/available-deferred-tools>/i);
21481
+ const body = block ? block[1] : value;
21482
+ const names = [];
21483
+ const lineRe = /^\s*-\s+([A-Za-z0-9_.:-]+)/gm;
21484
+ let m;
21485
+ while (m = lineRe.exec(body)) names.push(m[1]);
21486
+ const servers = /* @__PURE__ */ new Set();
21487
+ for (const name of names) {
21488
+ const seg = name.startsWith("mcp__") ? name.slice(5) : name;
21489
+ const server = seg.split("__")[0];
21490
+ if (server) servers.add(server);
21491
+ }
21492
+ const count = names.length;
21493
+ const label = servers.size === 1 ? [...servers][0] : servers.size ? `${servers.size} MCP servers` : "MCP";
21494
+ if (!count) return `MCP tools available: ${label}`;
21495
+ return `MCP tools available: ${label} (${count} ${count === 1 ? "tool" : "tools"})`;
21496
+ }
21497
+
21177
21498
  // src/tui/engine/boot-profile.mjs
21178
21499
  import { performance } from "node:perf_hooks";
21179
21500
  var BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
@@ -21959,8 +22280,8 @@ function resolveTuiRuntimeNotificationDelivery(event, text) {
21959
22280
 
21960
22281
  // src/tui/engine/render-timing.mjs
21961
22282
  var RENDER_THROTTLE_FLUSH_MS = 12;
21962
- var yieldToRenderer = () => new Promise((resolve6) => {
21963
- setTimeout(resolve6, RENDER_THROTTLE_FLUSH_MS);
22283
+ var yieldToRenderer = () => new Promise((resolve5) => {
22284
+ setTimeout(resolve5, RENDER_THROTTLE_FLUSH_MS);
21964
22285
  });
21965
22286
 
21966
22287
  // src/tui/engine/tool-result-status.mjs
@@ -22138,9 +22459,9 @@ function createToolApproval({ getState, set, nextId: nextId2, getDisposed, timeo
22138
22459
  }
22139
22460
  function requestToolApproval(input = {}) {
22140
22461
  if (getDisposed()) return Promise.resolve({ approved: false, reason: "runtime disposed" });
22141
- return new Promise((resolve6) => {
22462
+ return new Promise((resolve5) => {
22142
22463
  const id = nextId2();
22143
- toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve: resolve6, timer: null });
22464
+ toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve: resolve5, timer: null });
22144
22465
  presentNextToolApproval();
22145
22466
  });
22146
22467
  }
@@ -23002,7 +23323,7 @@ function createSessionFlow(bag) {
23002
23323
  const startedAt = Date.now();
23003
23324
  set({ commandBusy: true, commandStatus: { active: true, verb, startedAt, mode: "auto-clear" } });
23004
23325
  try {
23005
- await new Promise((resolve6) => setTimeout(resolve6, 0));
23326
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
23006
23327
  let compactType = null;
23007
23328
  if (useCompaction) {
23008
23329
  const compaction = runtime.getCompactionSettings();
@@ -24515,6 +24836,26 @@ import { readFileSync as readFileSync6 } from "node:fs";
24515
24836
  import { dirname as dirname8, join as join9 } from "node:path";
24516
24837
  import { fileURLToPath as fileURLToPath3 } from "node:url";
24517
24838
  var GLYPH = { ok: "\u2713", warn: "\u26A0", fail: "\u2717" };
24839
+ var CKPT_WRITE_WARN_S = 10;
24840
+ var CKPT_WRITE_MEDIAN_S = 5;
24841
+ function parseCheckpointWriteSeconds(logText) {
24842
+ const lines = String(logText).split(/\r?\n/).slice(-200);
24843
+ const out = [];
24844
+ for (const line of lines) {
24845
+ const m = /checkpoint complete:.*?\bwrite=([\d.]+)\s*s/i.exec(line);
24846
+ if (m) {
24847
+ const v = Number(m[1]);
24848
+ if (Number.isFinite(v)) out.push(v);
24849
+ }
24850
+ }
24851
+ return out;
24852
+ }
24853
+ function median(nums) {
24854
+ if (!nums.length) return 0;
24855
+ const s = [...nums].sort((a, b) => a - b);
24856
+ const mid = s.length >> 1;
24857
+ return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
24858
+ }
24518
24859
  function readPackageJson() {
24519
24860
  try {
24520
24861
  const dir = dirname8(fileURLToPath3(import.meta.url));
@@ -24648,6 +24989,31 @@ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
24648
24989
  const enabled = h.enabled === true;
24649
24990
  row("ok", `${enabled ? "enabled" : "disabled"} \xB7 ${events.length} event${events.length === 1 ? "" : "s"}`);
24650
24991
  });
24992
+ if (process.platform === "win32") {
24993
+ await check("defender", async (row) => {
24994
+ const dataDir = resolvePluginData();
24995
+ const pgdata = join9(dataDir, "pgdata");
24996
+ let writes;
24997
+ try {
24998
+ writes = parseCheckpointWriteSeconds(readFileSync6(join9(dataDir, "pg.log"), "utf8"));
24999
+ } catch {
25000
+ row("ok", "pg.log unavailable \xB7 check skipped");
25001
+ return;
25002
+ }
25003
+ if (!writes.length) {
25004
+ row("ok", "no checkpoint timings yet");
25005
+ return;
25006
+ }
25007
+ const max = Math.max(...writes);
25008
+ const med = median(writes);
25009
+ if (max <= CKPT_WRITE_WARN_S && med <= CKPT_WRITE_MEDIAN_S) {
25010
+ row("ok", `checkpoint write median ${med.toFixed(1)}s \xB7 max ${max.toFixed(1)}s`);
25011
+ return;
25012
+ }
25013
+ const fix = `Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-Command',"Add-MpPreference -ExclusionPath '${pgdata}'"`;
25014
+ row("warn", `slow checkpoints (median ${med.toFixed(1)}s \xB7 max ${max.toFixed(1)}s) suggest Defender real-time scan of ${pgdata}. Fix (run in PowerShell): ${fix}`);
25015
+ });
25016
+ }
24651
25017
  return ["mixdog doctor \u2014 installation health", ...rows].join("\n");
24652
25018
  }
24653
25019
 
@@ -24934,17 +25300,18 @@ function createEngineApiA(bag) {
24934
25300
  }
24935
25301
  },
24936
25302
  setMcpServerEnabled: async (name, enabled) => {
24937
- if (getState().commandBusy) return null;
24938
- set({ commandBusy: true });
24939
- try {
24940
- const status = await runtime.setMcpServerEnabled?.(name, enabled);
25303
+ const status = await runtime.setMcpServerEnabled?.(name, enabled);
25304
+ setImmediate(() => {
24941
25305
  resetStatsAndSyncContext();
24942
25306
  set({ ...routeState(), stats: { ...getState().stats } });
25307
+ });
25308
+ const row = status?.servers?.find((s) => s.name === name);
25309
+ if (enabled && row && (row.status === "failed" || row.error)) {
25310
+ pushNotice(`mcp enable failed: ${name}${row.error ? ` \u2014 ${row.error}` : ""}`, "error");
25311
+ } else {
24943
25312
  pushNotice(`mcp ${enabled ? "enabled" : "disabled"}: ${name}`, "info");
24944
- return status;
24945
- } finally {
24946
- set({ commandBusy: false });
24947
25313
  }
25314
+ return status;
24948
25315
  },
24949
25316
  getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
24950
25317
  setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
@@ -25098,7 +25465,7 @@ function createEngineApiA(bag) {
25098
25465
  const startedAt = Date.now();
25099
25466
  set({ commandBusy: true, commandStatus: { active: true, verb: "Running diagnostics", startedAt, mode: "doctor" } });
25100
25467
  try {
25101
- await new Promise((resolve6) => setTimeout(resolve6, 0));
25468
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
25102
25469
  const report = await buildDoctorReport(runtime, getState);
25103
25470
  pushNotice(report, "info");
25104
25471
  return report;
@@ -25118,7 +25485,7 @@ function createEngineApiA(bag) {
25118
25485
  const startedAt = Date.now();
25119
25486
  set({ commandBusy: true, commandStatus: { active: true, verb: "Compacting conversation", startedAt, mode: "compacting" } });
25120
25487
  try {
25121
- await new Promise((resolve6) => setTimeout(resolve6, 0));
25488
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
25122
25489
  const result = await runtime.compact({ recoverAgent: true });
25123
25490
  syncContextStats({ allowEstimated: true });
25124
25491
  set({ ...routeState(), stats: { ...getState().stats } });
@@ -25412,6 +25779,10 @@ async function createEngineSession({
25412
25779
  const pushUserOrSyntheticItem = (text, id = nextId(), origin = "user") => {
25413
25780
  if (origin === "injected" && isLikelyToolCompletionWrapper(text)) return;
25414
25781
  if (isModelVisibleToolCompletionWrapper(text)) return;
25782
+ if (isLateToolAnnouncement(text)) {
25783
+ pushItem({ kind: "notice", id, text: summarizeLateToolAnnouncement(text), tone: "info" });
25784
+ return;
25785
+ }
25415
25786
  if (upsertSyntheticToolItem(text, id)) return;
25416
25787
  pushItem({ kind: "user", id, text });
25417
25788
  };
@@ -25610,7 +25981,7 @@ function signalExitCode(signal, fallback = 1) {
25610
25981
  }
25611
25982
  function waitWithTimeout(promise, timeoutMs, label = "cleanup") {
25612
25983
  const ms = Math.max(1, Math.floor(Number(timeoutMs) || 1));
25613
- return new Promise((resolve6, reject) => {
25984
+ return new Promise((resolve5, reject) => {
25614
25985
  let settled = false;
25615
25986
  const timer = setTimeout(() => {
25616
25987
  if (settled) return;
@@ -25622,7 +25993,7 @@ function waitWithTimeout(promise, timeoutMs, label = "cleanup") {
25622
25993
  if (settled) return;
25623
25994
  settled = true;
25624
25995
  clearTimeout(timer);
25625
- resolve6(value);
25996
+ resolve5(value);
25626
25997
  }).catch((error) => {
25627
25998
  if (settled) return;
25628
25999
  settled = true;
@@ -25736,54 +26107,14 @@ function installProcessSignalCleanup({
25736
26107
  };
25737
26108
  }
25738
26109
 
25739
- // src/runtime/channels/lib/runtime-paths.mjs
25740
- import { tmpdir as tmpdir2 } from "os";
25741
- import { basename as basename4, join as join10, resolve as resolve5 } from "path";
25742
-
25743
- // src/runtime/channels/lib/state-file.mjs
25744
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25745
- function ensureDir2(dirPath) {
25746
- mkdirSync6(dirPath, { recursive: true });
25747
- }
25748
-
25749
- // src/runtime/channels/lib/runtime-paths.mjs
25750
- var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve5(process.env.MIXDOG_RUNTIME_ROOT) : join10(tmpdir2(), "mixdog");
25751
- var OWNER_DIR = join10(RUNTIME_ROOT, "owners");
25752
- var ACTIVE_INSTANCE_FILE = join10(RUNTIME_ROOT, "active-instance.json");
25753
- var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
25754
- var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
25755
- var TMP_FILE_TTL = 60 * 60 * 1e3;
25756
- function sanitize(value) {
25757
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
25758
- }
25759
- function ensureRuntimeDirs() {
25760
- ensureDir2(RUNTIME_ROOT);
25761
- ensureDir2(OWNER_DIR);
25762
- }
25763
- var UI_HEARTBEAT_STALE_MS = 5 * 60 * 1e3;
25764
- function touchUiHeartbeat(instanceId) {
25765
- ensureRuntimeDirs();
25766
- try {
25767
- updateJsonAtomicSync(ACTIVE_INSTANCE_FILE, (curRaw) => {
25768
- if (!curRaw) return void 0;
25769
- if (instanceId && curRaw.instanceId !== instanceId) return void 0;
25770
- return { ...curRaw, ui_heartbeat_at: Date.now(), updatedAt: Date.now() };
25771
- }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate", timeoutMs: 0 });
25772
- } catch {
25773
- }
25774
- }
25775
- var SERVER_PID_FILE = join10(
25776
- RUNTIME_ROOT,
25777
- `server-${sanitize(resolvePluginData() ?? "default")}.pid`
25778
- );
25779
-
25780
26110
  // src/tui/index.jsx
25781
26111
  var import_mixdog_debug = __toESM(require_mixdog_debug(), 1);
25782
26112
  import { jsx as jsx21 } from "react/jsx-runtime";
25783
- var TERMINAL_MODE_RESET = "\x1B[?1006l\x1B[?1005l\x1B[?1015l\x1B[?1003l\x1B[?1002l\x1B[?1000l\x1B[?2004l\x1B[?25h";
26113
+ var TERMINAL_MODE_RESET = "\x1B[?1006l\x1B[?1005l\x1B[?1015l\x1B[?1003l\x1B[?1002l\x1B[?1000l\x1B[?2004l\x1B[>0s\x1B[?25h";
25784
26114
  var TERMINAL_OSC_RESET_BG = "\x1B]111\x07";
25785
26115
  var TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace("\x1B[?25h", "\x1B[?25l");
25786
- var MOUSE_TRACKING_ON2 = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
26116
+ var XTSHIFTESCAPE_ON = process.env.WT_SESSION ? "" : "\x1B[>1s";
26117
+ var MOUSE_TRACKING_ON2 = `\x1B[?1000h\x1B[?1002h\x1B[?1006h${XTSHIFTESCAPE_ON}`;
25787
26118
  var BOOT_PROFILE_ENABLED2 = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
25788
26119
  var BOOT_PROFILE_START2 = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance3.now());
25789
26120
  var EXIT_WAIT_TIMEOUT_MS = positiveIntEnv2("MIXDOG_TUI_EXIT_WAIT_MS", 2500);
@@ -25972,8 +26303,8 @@ function waitWithTimeout2(promise, ms) {
25972
26303
  let timer = null;
25973
26304
  return Promise.race([
25974
26305
  Promise.resolve(promise).then(() => true),
25975
- new Promise((resolve6) => {
25976
- timer = setTimeout(() => resolve6(false), ms);
26306
+ new Promise((resolve5) => {
26307
+ timer = setTimeout(() => resolve5(false), ms);
25977
26308
  })
25978
26309
  ]).finally(() => {
25979
26310
  if (timer) clearTimeout(timer);
@@ -25992,7 +26323,7 @@ function scheduleHardExit(code = 0) {
25992
26323
  timer.unref?.();
25993
26324
  }
25994
26325
  function resolveTuiStderrLogPath() {
25995
- return process.env.MIXDOG_TUI_STDERR_LOG || join11(process.env.MIXDOG_RUNTIME_ROOT || join11(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
26326
+ return process.env.MIXDOG_TUI_STDERR_LOG || join10(process.env.MIXDOG_RUNTIME_ROOT || join10(tmpdir2(), "mixdog"), "mixdog-tui.stderr.log");
25996
26327
  }
25997
26328
  function ansiFg(rgb) {
25998
26329
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -26074,7 +26405,7 @@ function installTuiStderrGuard() {
26074
26405
  const originalWrite = process.stderr.write.bind(process.stderr);
26075
26406
  const logPath = resolveTuiStderrLogPath();
26076
26407
  try {
26077
- mkdirSync7(dirname9(logPath), { recursive: true });
26408
+ mkdirSync6(dirname9(logPath), { recursive: true });
26078
26409
  } catch {
26079
26410
  }
26080
26411
  (0, import_mixdog_debug.rotateBoundedLog)(logPath, import_mixdog_debug.PLUGIN_LOG_MAX_BYTES, import_mixdog_debug.PLUGIN_LOG_KEEP_BYTES);
@@ -26119,6 +26450,23 @@ function installTuiStderrGuard() {
26119
26450
  }
26120
26451
  };
26121
26452
  }
26453
+ function installTuiConsoleGuard() {
26454
+ const methods = ["log", "info", "warn", "error", "debug", "trace"];
26455
+ const original = /* @__PURE__ */ new Map();
26456
+ for (const m of methods) {
26457
+ original.set(m, console[m]);
26458
+ console[m] = (...args) => {
26459
+ try {
26460
+ process.stderr.write(`[console.${m}] ${format(...args)}
26461
+ `);
26462
+ } catch {
26463
+ }
26464
+ };
26465
+ }
26466
+ return () => {
26467
+ for (const [m, fn] of original) console[m] = fn;
26468
+ };
26469
+ }
26122
26470
  async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {}) {
26123
26471
  const startedAt = performance3.now();
26124
26472
  bootProfile2("run:start", { provider, model, toolMode, remote });
@@ -26129,6 +26477,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26129
26477
  return 1;
26130
26478
  }
26131
26479
  const restoreStderr = installTuiStderrGuard();
26480
+ const restoreConsole = installTuiConsoleGuard();
26132
26481
  const stopPerfProbe = installTuiPerfProbe();
26133
26482
  const stopLoopProbe = installTuiLoopProbe();
26134
26483
  const restorePrimedInput = () => drainStdin(process.stdin);
@@ -26169,6 +26518,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26169
26518
  process.off("exit", restoreTerminal);
26170
26519
  } catch {
26171
26520
  }
26521
+ restoreConsole();
26172
26522
  restoreStderr();
26173
26523
  process.stderr.write(`mixdog: ${error?.message || error}
26174
26524
  `);
@@ -26194,20 +26544,10 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26194
26544
  afterCleanup: (reason) => {
26195
26545
  restoreTerminal();
26196
26546
  dumpActiveHandles(`after-${reason}`);
26547
+ restoreConsole();
26197
26548
  restoreStderr();
26198
26549
  }
26199
26550
  });
26200
- const uiHeartbeatTimer = setInterval(() => {
26201
- try {
26202
- touchUiHeartbeat();
26203
- } catch {
26204
- }
26205
- }, 3e4);
26206
- if (typeof uiHeartbeatTimer.unref === "function") uiHeartbeatTimer.unref();
26207
- try {
26208
- touchUiHeartbeat();
26209
- } catch {
26210
- }
26211
26551
  const STDIO_DEATH_FATAL_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED", "EIO"]);
26212
26552
  const stdioDeathListeners = [];
26213
26553
  const registerStdioDeath = (stream, event, { requireCode = false } = {}) => {
@@ -26231,7 +26571,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26231
26571
  registerStdioDeath(process.stderr, "error", { requireCode: true });
26232
26572
  registerStdioDeath(process.stderr, "close");
26233
26573
  try {
26234
- const instance = render(/* @__PURE__ */ jsx21(App, { store, forceOnboarding: forceOnboarding === true }), { exitOnCtrlC: false, maxFps: 60, incrementalRendering: true, onRender: makeRenderProfiler() });
26574
+ const instance = render(/* @__PURE__ */ jsx21(App, { store, forceOnboarding: forceOnboarding === true }), { exitOnCtrlC: false, maxFps: 60, incrementalRendering: true, patchConsole: false, onRender: makeRenderProfiler() });
26235
26575
  bootProfile2("render:mounted", { ms: (performance3.now() - startedAt).toFixed(1) });
26236
26576
  const { waitUntilExit } = instance;
26237
26577
  if (mouseTracking && typeof instance.setSelection === "function") {
@@ -26249,11 +26589,13 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26249
26589
  if (mouseTracking && typeof instance.getSelectionRows === "function") {
26250
26590
  store.getRenderSelectionRows = instance.getSelectionRows;
26251
26591
  }
26592
+ if (mouseTracking && typeof instance.forceFullRepaint === "function") {
26593
+ store.forceRenderRepaint = instance.forceFullRepaint;
26594
+ }
26252
26595
  await waitUntilExit();
26253
26596
  } finally {
26254
26597
  stopPerfProbe();
26255
26598
  stopLoopProbe();
26256
- clearInterval(uiHeartbeatTimer);
26257
26599
  for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
26258
26600
  try {
26259
26601
  stream.off(event, handler);
@@ -26268,6 +26610,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26268
26610
  }
26269
26611
  restoreTerminal();
26270
26612
  dumpActiveHandles("after-restore");
26613
+ restoreConsole();
26271
26614
  restoreStderr();
26272
26615
  }
26273
26616
  scheduleHardExit(0);