mixdog 0.9.22 → 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 (91) hide show
  1. package/README.md +1 -4
  2. package/package.json +1 -1
  3. package/scripts/build-runtime-windows.ps1 +242 -242
  4. package/scripts/channel-daemon-smoke.mjs +165 -0
  5. package/scripts/channel-daemon-stub.mjs +69 -0
  6. package/scripts/recall-usecase-cases.json +1 -1
  7. package/scripts/smoke-runtime-negative.ps1 +106 -106
  8. package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
  9. package/scripts/tool-efficiency-diag.mjs +1 -1
  10. package/scripts/tool-smoke.mjs +30 -17
  11. package/src/rules/lead/02-channels.md +3 -3
  12. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +187 -0
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +40 -3
  14. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
  16. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
  17. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
  20. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
  21. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
  23. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -0
  24. package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
  25. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +24 -11
  27. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
  28. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -1
  29. package/src/runtime/channels/lib/owned-runtime.mjs +126 -167
  30. package/src/runtime/channels/lib/owner-heartbeat.mjs +11 -60
  31. package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
  32. package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
  33. package/src/runtime/channels/lib/seat-lock.mjs +196 -0
  34. package/src/runtime/channels/lib/session-discovery.mjs +2 -1
  35. package/src/runtime/channels/lib/tool-dispatch.mjs +24 -8
  36. package/src/runtime/channels/lib/worker-main.mjs +42 -11
  37. package/src/runtime/memory/index.mjs +54 -7
  38. package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
  39. package/src/runtime/memory/lib/pg/process.mjs +85 -40
  40. package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
  41. package/src/runtime/shared/atomic-file.mjs +20 -4
  42. package/src/runtime/shared/tool-primitives.mjs +31 -1
  43. package/src/runtime/shared/tool-surface.mjs +23 -13
  44. package/src/session-runtime/config-lifecycle.mjs +48 -7
  45. package/src/session-runtime/lifecycle-api.mjs +9 -0
  46. package/src/session-runtime/mcp-glue.mjs +63 -1
  47. package/src/session-runtime/resource-api.mjs +62 -8
  48. package/src/session-runtime/runtime-core.mjs +32 -2
  49. package/src/session-runtime/session-text.mjs +41 -0
  50. package/src/session-runtime/session-turn-api.mjs +24 -0
  51. package/src/session-runtime/settings-api.mjs +8 -1
  52. package/src/session-runtime/tool-catalog.mjs +306 -38
  53. package/src/session-runtime/tool-defs.mjs +7 -7
  54. package/src/session-runtime/workflow.mjs +2 -1
  55. package/src/standalone/channel-daemon-client.mjs +224 -0
  56. package/src/standalone/channel-daemon-transport.mjs +351 -0
  57. package/src/standalone/channel-daemon.mjs +139 -0
  58. package/src/standalone/channel-worker.mjs +213 -4
  59. package/src/standalone/hook-bus.mjs +71 -3
  60. package/src/tui/App.jsx +105 -17
  61. package/src/tui/app/clipboard.mjs +39 -19
  62. package/src/tui/app/doctor.mjs +57 -0
  63. package/src/tui/app/extension-pickers.mjs +53 -9
  64. package/src/tui/app/maintenance-pickers.mjs +0 -5
  65. package/src/tui/app/slash-dispatch.mjs +4 -4
  66. package/src/tui/app/text-layout.mjs +11 -0
  67. package/src/tui/app/use-mouse-input.mjs +235 -51
  68. package/src/tui/app/use-prompt-handlers.mjs +49 -30
  69. package/src/tui/app/use-transcript-scroll.mjs +124 -27
  70. package/src/tui/app/use-transcript-window.mjs +55 -1
  71. package/src/tui/components/Message.jsx +1 -1
  72. package/src/tui/components/PromptInput.jsx +3 -1
  73. package/src/tui/components/QueuedCommands.jsx +21 -10
  74. package/src/tui/components/ToolExecution.jsx +2 -2
  75. package/src/tui/dist/index.mjs +635 -306
  76. package/src/tui/engine/session-api.mjs +17 -7
  77. package/src/tui/engine/session-flow.mjs +6 -0
  78. package/src/tui/engine.mjs +8 -0
  79. package/src/tui/index.jsx +62 -18
  80. package/src/tui/paste-attachments.mjs +26 -0
  81. package/src/ui/statusline.mjs +45 -5
  82. package/src/ui/tool-card.mjs +8 -1
  83. package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
  84. package/src/workflows/bench/WORKFLOW.md +46 -0
  85. package/src/workflows/default/WORKFLOW.md +6 -0
  86. package/src/workflows/solo/WORKFLOW.md +5 -0
  87. package/vendor/ink/build/ink.js +23 -1
  88. package/vendor/ink/build/output.js +154 -71
  89. package/vendor/ink/build/render-node-to-output.js +44 -2
  90. package/vendor/ink/build/render.js +4 -0
  91. 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;
@@ -14006,8 +14272,8 @@ function _releaseReclaimGuard(reclaim) {
14006
14272
  }
14007
14273
  }
14008
14274
  function _asyncSleep(ms) {
14009
- return new Promise((resolve6) => {
14010
- setTimeout(resolve6, Math.max(1, Number(ms) || 1));
14275
+ return new Promise((resolve5) => {
14276
+ setTimeout(resolve5, Math.max(1, Number(ms) || 1));
14011
14277
  });
14012
14278
  }
14013
14279
  async function withFileLock(lockPath, fn, opts = {}) {
@@ -14025,7 +14291,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14025
14291
  lastErr = err;
14026
14292
  if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
14027
14293
  if (timeoutMs <= 0) {
14028
- const contErr = new Error(`atomic lock contended (try-once): ${lockPath}`);
14294
+ const contErr = new Error(`atomic lock contended (try-once): ${lockPath} [${_describeLockHolder(lockPath)}]`);
14029
14295
  contErr.code = "ELOCKCONTENDED";
14030
14296
  contErr.cause = err;
14031
14297
  throw contErr;
@@ -14060,7 +14326,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14060
14326
  }
14061
14327
  }
14062
14328
  }
14063
- 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)}]`);
14064
14330
  timeoutErr.code = "ELOCKTIMEOUT";
14065
14331
  timeoutErr.cause = lastErr;
14066
14332
  throw timeoutErr;
@@ -14210,21 +14476,6 @@ function writeFileAtomicSync(filePath, data, opts = {}) {
14210
14476
  function writeJsonAtomicSync(filePath, value, opts = {}) {
14211
14477
  return writeFileAtomicSync(filePath, JSON.stringify(value, null, opts.compact ? 0 : 2) + "\n", opts);
14212
14478
  }
14213
- function updateJsonAtomicSync(filePath, mutator, opts = {}) {
14214
- const { lock: _lock, ...writeOpts } = opts;
14215
- return withFileLockSync(`${filePath}.lock`, () => {
14216
- let cur = null;
14217
- try {
14218
- cur = JSON.parse(readFileSync2(filePath, "utf8"));
14219
- } catch {
14220
- cur = null;
14221
- }
14222
- const next = mutator(cur);
14223
- if (next === void 0) return cur;
14224
- writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
14225
- return next;
14226
- }, opts);
14227
- }
14228
14479
  async function updateJsonAtomic(filePath, mutator, opts = {}) {
14229
14480
  const { lock: _lock, ...writeOpts } = opts;
14230
14481
  return withFileLock(`${filePath}.lock`, () => {
@@ -15174,7 +15425,7 @@ function createModelPicker({
15174
15425
  items: [],
15175
15426
  onCancel: cancelModelPicker
15176
15427
  });
15177
- await new Promise((resolve6) => setTimeout(resolve6, 0));
15428
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
15178
15429
  try {
15179
15430
  if (options.refreshModels !== true && options.cacheRef !== "search") {
15180
15431
  refreshModelsPromise = Promise.resolve(loadModels({ force: false }));
@@ -15500,7 +15751,7 @@ function createProviderSetupPicker({
15500
15751
  }
15501
15752
  });
15502
15753
  try {
15503
- await new Promise((resolve6) => setTimeout(resolve6, 0));
15754
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
15504
15755
  setup = await store.getProviderSetup();
15505
15756
  } catch (e) {
15506
15757
  store.pushNotice(`providers failed: ${e?.message || e}`, "error");
@@ -16886,10 +17137,6 @@ function createSlashDispatch({
16886
17137
  return true;
16887
17138
  }
16888
17139
  case "search":
16889
- if (state.busy) {
16890
- store.pushNotice("wait for the current turn to finish before /search", "warn");
16891
- return false;
16892
- }
16893
17140
  if (arg) store.pushNotice("/search sets the search provider/model; the search tool uses that model when called.", "warn");
16894
17141
  openSearchPicker();
16895
17142
  return true;
@@ -17290,57 +17537,64 @@ function usePromptHandlers({
17290
17537
  const handlePromptPaste = useCallback5((text, meta = {}) => {
17291
17538
  const source = String(meta?.source || "paste");
17292
17539
  const value = String(text ?? "");
17293
- if (source === "clipboard-image-shortcut" && !value) {
17294
- return readClipboardImageAttachment().then((image) => {
17295
- if (!image) {
17296
- showPromptHint("no image found on clipboard", "plain");
17297
- 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;
17558
+ }
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
+ }
17298
17574
  }
17299
- const ref = registerPastedImage(image);
17300
- showPromptHint(`attached ${image.filename || "clipboard image"}`, "plain");
17301
- return ref;
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
+ });
17302
17592
  }).catch((e) => {
17303
- showPromptHint(`image paste failed: ${e?.message || e}`, "warn");
17593
+ showPromptHint(`paste failed: ${e?.message || e}`, "warn");
17304
17594
  return false;
17305
17595
  });
17306
17596
  }
17307
- const chunks = splitPastedImagePathCandidates(value);
17308
- const hasImagePath = chunks.some((chunk) => chunk.imagePath);
17309
- if (!hasImagePath) {
17310
- if (shouldFoldPastedText(value)) return registerPastedText(value);
17311
- return void 0;
17312
- }
17313
- return Promise.all(chunks.map(async (chunk) => {
17314
- if (!chunk.imagePath) return chunk.text;
17315
- try {
17316
- const image = await readImageAttachmentFromPath(chunk.text, state.cwd || process.cwd());
17317
- if (!image) return chunk.text;
17318
- const ref = registerPastedImage(image);
17319
- showPromptHint(`attached ${image.filename || "image"}`, "plain");
17320
- return ref;
17321
- } catch (e) {
17322
- showPromptHint(`image attach failed: ${e?.message || e}`, "warn");
17323
- return chunk.text;
17324
- }
17325
- })).then((parts) => {
17326
- let out = "";
17327
- let run = "";
17328
- const flushRun = () => {
17329
- if (!run) return;
17330
- out += shouldFoldPastedText(run) ? registerPastedText(run) : run;
17331
- run = "";
17332
- };
17333
- for (let i = 0; i < chunks.length; i += 1) {
17334
- if (chunks[i].imagePath) {
17335
- flushRun();
17336
- out += parts[i];
17337
- } else {
17338
- run += parts[i];
17339
- }
17340
- }
17341
- flushRun();
17342
- return out;
17343
- });
17597
+ return processText(value);
17344
17598
  }, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
17345
17599
  const handlePromptHistoryNavigate = useCallback5((direction, currentText = "", meta = {}) => {
17346
17600
  const currentValue = String(currentText || "");
@@ -17689,7 +17943,7 @@ var AssistantMessage = React14.memo(function AssistantMessage2({
17689
17943
  });
17690
17944
  var UserMessage = React14.memo(function UserMessage2({ text, attached = false, columns, themeEpoch = 0 }) {
17691
17945
  const bandColumns = Math.max(1, columns - 1);
17692
- 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 }) });
17693
17947
  });
17694
17948
  function NoticeMessage({ text, tone, columns = 80 }) {
17695
17949
  const accentColor = tone === "error" ? theme.error : tone === "warn" ? theme.warning : theme.inactive;
@@ -18378,13 +18632,13 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18378
18632
  else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
18379
18633
  else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : "") || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
18380
18634
  labelText = safeInlineText(labelText);
18381
- const toolSearchSummary = !pending && normalizedName === "tool_search" && hasResult ? toolSearchLoadedSummary(displayedResultText) : "";
18635
+ const toolSearchSummary = !pending && normalizedName === "load_tool" && hasResult ? toolSearchLoadedSummary(displayedResultText) : "";
18382
18636
  const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse ? "" : toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary));
18383
18637
  const summaryIsHeaderCount = rawSummaryText && /^\d+\s+\S+$/.test(rawSummaryText) && labelText.endsWith(rawSummaryText);
18384
18638
  const summaryText = summaryIsHeaderCount ? "" : rawSummaryText;
18385
18639
  const agentHasExpandableBody = isAgentSurfaceCard && !pending && hasResult && (isAgentResponse || totalLines > 1 || firstResultLineClipped);
18386
18640
  const shellHasExpandableBody = isShellSurface && !pending && hasDisplayResult && hasDisplayBody && (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
18387
- const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : isAgentSurfaceCard ? agentHasExpandableBody : hasHiddenDetail || backgroundMetadataExpandable) && normalizedName !== "tool_search";
18641
+ const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : isAgentSurfaceCard ? agentHasExpandableBody : hasHiddenDetail || backgroundMetadataExpandable) && normalizedName !== "load_tool";
18388
18642
  const expandHintColor = theme.subtle;
18389
18643
  const gutter = 2;
18390
18644
  const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? "collapse" : "expand"}` : "";
@@ -18616,7 +18870,9 @@ var PANEL_LAYOUT_SIG = {
18616
18870
  TEXT: 5,
18617
18871
  // Prompt-wrap/meta row counts (trailing churn tokens, see token order note
18618
18872
  // below). PROMPT_META is the 2-row live-spinner band slot.
18619
- PROMPT_META: 9
18873
+ PROMPT_META: 9,
18874
+ // Queued steering band rows (full wrapped height, see queuedBandRows).
18875
+ QUEUED: 10
18620
18876
  };
18621
18877
  var PROJECT_TEXT_ENTRY_KINDS = /* @__PURE__ */ new Set(["project-new", "project-create-confirm", "project-rename"]);
18622
18878
  var CORE_MULTILINE_TEXT_ENTRY_KINDS = /* @__PURE__ */ new Set(["core-add", "core-edit"]);
@@ -18678,7 +18934,9 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18678
18934
  const projectPickerRef = useRef9(null);
18679
18935
  const buildProjectPickerState = (opts) => projectPickerRef.current.buildProjectPickerState(opts);
18680
18936
  const [picker, setPickerState] = useState8(null);
18937
+ const livePickerRef = useRef9(null);
18681
18938
  const setPicker = useCallback6((next) => {
18939
+ livePickerRef.current = typeof next === "function" ? next(livePickerRef.current) : next;
18682
18940
  setPickerState((prev) => {
18683
18941
  const resolved = typeof next === "function" ? next(prev) : next;
18684
18942
  if (resolved && typeof resolved === "object" && pickerOpenedFromEnterRef.current) {
@@ -18689,9 +18947,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18689
18947
  }
18690
18948
  return resolved.indexMode ? resolved : { ...resolved, indexMode: "always" };
18691
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
+ }
18692
18953
  return resolved;
18693
18954
  });
18694
18955
  }, []);
18956
+ livePickerRef.current = picker;
18695
18957
  const [contextPanel, setContextPanel] = useState8(null);
18696
18958
  const [usagePanel, setUsagePanel] = useState8(null);
18697
18959
  const usageRequestRef = useRef9(0);
@@ -18733,7 +18995,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18733
18995
  } catch {
18734
18996
  }
18735
18997
  if (!onboardingOwnsScreen && state.items.length === 0) {
18736
- setPickerState(projectPicker.buildProjectPickerState({ initialEntry: true }));
18998
+ setPicker(projectPicker.buildProjectPickerState({ initialEntry: true }));
18737
18999
  }
18738
19000
  }
18739
19001
  const {
@@ -18804,6 +19066,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18804
19066
  clean,
18805
19067
  copyToClipboard,
18806
19068
  setPicker,
19069
+ getPicker: () => livePickerRef.current,
18807
19070
  setProviderPrompt,
18808
19071
  setChannelPrompt,
18809
19072
  setHookPrompt,
@@ -19306,13 +19569,16 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
19306
19569
  const renderText = store.getRenderSelectionText?.();
19307
19570
  const remembered = selectionTextRef.current || "";
19308
19571
  let text = renderText == null ? remembered : remembered.length > renderText.length ? remembered : renderText;
19309
- const stitched = getStitchedSelectionText?.() || "";
19310
- 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;
19311
19574
  if ((!text || !text.trim()) && attempt < 4) {
19312
19575
  setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
19313
19576
  return;
19314
19577
  }
19315
- if (!text || !text.trim()) return;
19578
+ if (!text || !text.trim()) {
19579
+ showSelectionCopyHint("nothing to copy \xB7 select text first", "error");
19580
+ return;
19581
+ }
19316
19582
  selectionTextRef.current = text;
19317
19583
  copyToClipboard(text).then(() => {
19318
19584
  const lines = text.split("\n").length;
@@ -19388,8 +19654,8 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
19388
19654
  let timer = null;
19389
19655
  Promise.race([
19390
19656
  Promise.resolve(store.dispose?.("cli-react-exit", { detach: true })),
19391
- new Promise((resolve6) => {
19392
- timer = setTimeout(resolve6, 350);
19657
+ new Promise((resolve5) => {
19658
+ timer = setTimeout(resolve5, 350);
19393
19659
  })
19394
19660
  ]).finally(() => {
19395
19661
  if (timer) clearTimeout(timer);
@@ -20495,7 +20761,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20495
20761
  const promptMetaRows = promptMetaVisible ? 2 : 0;
20496
20762
  const overlayHintRequested = !inputBoxHidden && !hasFloatingPanel && !liveSpinner && !!inputHint && !queuedVisible;
20497
20763
  const overlayHintRows = 0;
20498
- 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;
20499
20771
  const INPUT_BOX_ROWS = promptBoxRows + promptMetaRows + overlayHintRows;
20500
20772
  const baseReserve = WELCOME_ROWS + SCROLL_HINT_ROWS + LIVE_STATUS_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows;
20501
20773
  const maxFloatingPanelRows = Math.max(0, resizeState.rows - baseReserve - 1);
@@ -20530,10 +20802,14 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20530
20802
  const nextMetaRows = Number(String(panelLayoutSignature).split("|")[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
20531
20803
  const doneTailAppendedThisCommit = latestDoneAtTail && (latestTranscriptItem?.id ?? null) !== panelTransition.tailId;
20532
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;
20533
20809
  const instantPanelClose = panelShrinkRows > 0 && (promptRowsOnlyChange || isInstantPanelCloseTransition(panelTransition.signature, panelLayoutSignature, initialProjectEntryClose));
20534
20810
  const slashOpenOnEmptyTranscript = initialProjectEntryClose && !panelSignatureFlags(panelTransition.signature).slash && panelSignatureFlags(panelLayoutSignature).slash;
20535
20811
  if (instantPanelClose) {
20536
- panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows);
20812
+ panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows - queuedPromoteCollapseRows);
20537
20813
  panelTransition.clearRows = 0;
20538
20814
  panelTransition.guardRows = 0;
20539
20815
  panelTransition.epoch = panelTransitionEpoch;
@@ -20562,8 +20838,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20562
20838
  const viewportHeight = Math.max(1, resizeState.rows - bottomReserve);
20563
20839
  const guardCapacityRows = Math.max(0, viewportHeight - 1);
20564
20840
  const baseGuardRows = guardCapacityRows > 0 ? 1 : 0;
20565
- const scrollGuardNearBottom = followingRef.current || scrollTargetRef.current <= baseGuardRows;
20566
- const scrollGuardRows = !scrollGuardNearBottom && scrollOffset > 0 && guardCapacityRows > baseGuardRows ? 1 : 0;
20841
+ const scrollGuardRows = 0;
20567
20842
  const transcriptGuardRows = Math.min(guardCapacityRows, baseGuardRows + panelTransitionGuardRows + scrollGuardRows);
20568
20843
  const welcomePromptHintText = conditionalWelcomePromptHint || welcomePromptHintRef.current || "";
20569
20844
  const welcomePromptHintVisible = Boolean(
@@ -20571,13 +20846,14 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20571
20846
  );
20572
20847
  const welcomePromptHintRows = welcomePromptHintVisible && viewportHeight - transcriptGuardRows >= 2 ? 1 : 0;
20573
20848
  welcomePromptHintVisibleRef.current = welcomePromptHintRows > 0;
20849
+ const overlayHintBandRows = overlayHintRequested && state.items.length === 0 && viewportHeight - transcriptGuardRows - welcomePromptHintRows >= 2 ? 1 : 0;
20574
20850
  const panelCloseMaskRows = Math.min(
20575
20851
  panelCloseInkMaskRows,
20576
- Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - 1)
20852
+ Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - overlayHintBandRows - 1)
20577
20853
  );
20578
20854
  const transcriptContentHeight = Math.max(
20579
20855
  1,
20580
- viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows
20856
+ viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows - overlayHintBandRows
20581
20857
  );
20582
20858
  const transcriptBottomSlackRows = Math.max(0, baseGuardRows);
20583
20859
  transcriptBottomSlackRowsRef.current = transcriptBottomSlackRows;
@@ -20799,9 +21075,13 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20799
21075
  backgroundColor: surfaceBackground()
20800
21076
  }
20801
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,
20802
21082
  transcriptGuardRows > 0 ? /* @__PURE__ */ jsxs16(Box18, { height: transcriptGuardRows, flexShrink: 0, backgroundColor: surfaceBackground(), flexDirection: "row", width: "100%", overflow: "hidden", children: [
20803
21083
  /* @__PURE__ */ jsx20(Box18, { flexGrow: 1, flexShrink: 1, overflow: "hidden" }),
20804
- 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
20805
21085
  ] }) : null
20806
21086
  ]
20807
21087
  }
@@ -21000,7 +21280,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
21000
21280
  ),
21001
21281
  /* @__PURE__ */ jsx20(Box18, { height: 1, width: "100%", backgroundColor: surfaceBackground() })
21002
21282
  ] }) : null,
21003
- queuedVisible ? /* @__PURE__ */ jsx20(QueuedCommands, { queued: state.queued, columns: frameColumns }) : null,
21283
+ queuedVisible ? /* @__PURE__ */ jsx20(QueuedCommands, { queued: state.queued, columns: frameColumns, compact: queuedCompact }) : null,
21004
21284
  /* @__PURE__ */ jsx20(
21005
21285
  Box18,
21006
21286
  {
@@ -21188,6 +21468,33 @@ function modelVisibleToolCompletionMessage(text, meta = {}) {
21188
21468
  ].join("\n");
21189
21469
  }
21190
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
+
21191
21498
  // src/tui/engine/boot-profile.mjs
21192
21499
  import { performance } from "node:perf_hooks";
21193
21500
  var BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
@@ -21973,8 +22280,8 @@ function resolveTuiRuntimeNotificationDelivery(event, text) {
21973
22280
 
21974
22281
  // src/tui/engine/render-timing.mjs
21975
22282
  var RENDER_THROTTLE_FLUSH_MS = 12;
21976
- var yieldToRenderer = () => new Promise((resolve6) => {
21977
- setTimeout(resolve6, RENDER_THROTTLE_FLUSH_MS);
22283
+ var yieldToRenderer = () => new Promise((resolve5) => {
22284
+ setTimeout(resolve5, RENDER_THROTTLE_FLUSH_MS);
21978
22285
  });
21979
22286
 
21980
22287
  // src/tui/engine/tool-result-status.mjs
@@ -22152,9 +22459,9 @@ function createToolApproval({ getState, set, nextId: nextId2, getDisposed, timeo
22152
22459
  }
22153
22460
  function requestToolApproval(input = {}) {
22154
22461
  if (getDisposed()) return Promise.resolve({ approved: false, reason: "runtime disposed" });
22155
- return new Promise((resolve6) => {
22462
+ return new Promise((resolve5) => {
22156
22463
  const id = nextId2();
22157
- toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve: resolve6, timer: null });
22464
+ toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve: resolve5, timer: null });
22158
22465
  presentNextToolApproval();
22159
22466
  });
22160
22467
  }
@@ -23016,7 +23323,7 @@ function createSessionFlow(bag) {
23016
23323
  const startedAt = Date.now();
23017
23324
  set({ commandBusy: true, commandStatus: { active: true, verb, startedAt, mode: "auto-clear" } });
23018
23325
  try {
23019
- await new Promise((resolve6) => setTimeout(resolve6, 0));
23326
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
23020
23327
  let compactType = null;
23021
23328
  if (useCompaction) {
23022
23329
  const compaction = runtime.getCompactionSettings();
@@ -24529,6 +24836,26 @@ import { readFileSync as readFileSync6 } from "node:fs";
24529
24836
  import { dirname as dirname8, join as join9 } from "node:path";
24530
24837
  import { fileURLToPath as fileURLToPath3 } from "node:url";
24531
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
+ }
24532
24859
  function readPackageJson() {
24533
24860
  try {
24534
24861
  const dir = dirname8(fileURLToPath3(import.meta.url));
@@ -24662,6 +24989,31 @@ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
24662
24989
  const enabled = h.enabled === true;
24663
24990
  row("ok", `${enabled ? "enabled" : "disabled"} \xB7 ${events.length} event${events.length === 1 ? "" : "s"}`);
24664
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
+ }
24665
25017
  return ["mixdog doctor \u2014 installation health", ...rows].join("\n");
24666
25018
  }
24667
25019
 
@@ -24948,17 +25300,18 @@ function createEngineApiA(bag) {
24948
25300
  }
24949
25301
  },
24950
25302
  setMcpServerEnabled: async (name, enabled) => {
24951
- if (getState().commandBusy) return null;
24952
- set({ commandBusy: true });
24953
- try {
24954
- const status = await runtime.setMcpServerEnabled?.(name, enabled);
25303
+ const status = await runtime.setMcpServerEnabled?.(name, enabled);
25304
+ setImmediate(() => {
24955
25305
  resetStatsAndSyncContext();
24956
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 {
24957
25312
  pushNotice(`mcp ${enabled ? "enabled" : "disabled"}: ${name}`, "info");
24958
- return status;
24959
- } finally {
24960
- set({ commandBusy: false });
24961
25313
  }
25314
+ return status;
24962
25315
  },
24963
25316
  getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
24964
25317
  setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
@@ -25112,7 +25465,7 @@ function createEngineApiA(bag) {
25112
25465
  const startedAt = Date.now();
25113
25466
  set({ commandBusy: true, commandStatus: { active: true, verb: "Running diagnostics", startedAt, mode: "doctor" } });
25114
25467
  try {
25115
- await new Promise((resolve6) => setTimeout(resolve6, 0));
25468
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
25116
25469
  const report = await buildDoctorReport(runtime, getState);
25117
25470
  pushNotice(report, "info");
25118
25471
  return report;
@@ -25132,7 +25485,7 @@ function createEngineApiA(bag) {
25132
25485
  const startedAt = Date.now();
25133
25486
  set({ commandBusy: true, commandStatus: { active: true, verb: "Compacting conversation", startedAt, mode: "compacting" } });
25134
25487
  try {
25135
- await new Promise((resolve6) => setTimeout(resolve6, 0));
25488
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
25136
25489
  const result = await runtime.compact({ recoverAgent: true });
25137
25490
  syncContextStats({ allowEstimated: true });
25138
25491
  set({ ...routeState(), stats: { ...getState().stats } });
@@ -25426,6 +25779,10 @@ async function createEngineSession({
25426
25779
  const pushUserOrSyntheticItem = (text, id = nextId(), origin = "user") => {
25427
25780
  if (origin === "injected" && isLikelyToolCompletionWrapper(text)) return;
25428
25781
  if (isModelVisibleToolCompletionWrapper(text)) return;
25782
+ if (isLateToolAnnouncement(text)) {
25783
+ pushItem({ kind: "notice", id, text: summarizeLateToolAnnouncement(text), tone: "info" });
25784
+ return;
25785
+ }
25429
25786
  if (upsertSyntheticToolItem(text, id)) return;
25430
25787
  pushItem({ kind: "user", id, text });
25431
25788
  };
@@ -25624,7 +25981,7 @@ function signalExitCode(signal, fallback = 1) {
25624
25981
  }
25625
25982
  function waitWithTimeout(promise, timeoutMs, label = "cleanup") {
25626
25983
  const ms = Math.max(1, Math.floor(Number(timeoutMs) || 1));
25627
- return new Promise((resolve6, reject) => {
25984
+ return new Promise((resolve5, reject) => {
25628
25985
  let settled = false;
25629
25986
  const timer = setTimeout(() => {
25630
25987
  if (settled) return;
@@ -25636,7 +25993,7 @@ function waitWithTimeout(promise, timeoutMs, label = "cleanup") {
25636
25993
  if (settled) return;
25637
25994
  settled = true;
25638
25995
  clearTimeout(timer);
25639
- resolve6(value);
25996
+ resolve5(value);
25640
25997
  }).catch((error) => {
25641
25998
  if (settled) return;
25642
25999
  settled = true;
@@ -25750,54 +26107,14 @@ function installProcessSignalCleanup({
25750
26107
  };
25751
26108
  }
25752
26109
 
25753
- // src/runtime/channels/lib/runtime-paths.mjs
25754
- import { tmpdir as tmpdir2 } from "os";
25755
- import { basename as basename4, join as join10, resolve as resolve5 } from "path";
25756
-
25757
- // src/runtime/channels/lib/state-file.mjs
25758
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25759
- function ensureDir2(dirPath) {
25760
- mkdirSync6(dirPath, { recursive: true });
25761
- }
25762
-
25763
- // src/runtime/channels/lib/runtime-paths.mjs
25764
- var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve5(process.env.MIXDOG_RUNTIME_ROOT) : join10(tmpdir2(), "mixdog");
25765
- var OWNER_DIR = join10(RUNTIME_ROOT, "owners");
25766
- var ACTIVE_INSTANCE_FILE = join10(RUNTIME_ROOT, "active-instance.json");
25767
- var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
25768
- var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
25769
- var TMP_FILE_TTL = 60 * 60 * 1e3;
25770
- function sanitize(value) {
25771
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
25772
- }
25773
- function ensureRuntimeDirs() {
25774
- ensureDir2(RUNTIME_ROOT);
25775
- ensureDir2(OWNER_DIR);
25776
- }
25777
- var UI_HEARTBEAT_STALE_MS = 5 * 60 * 1e3;
25778
- function touchUiHeartbeat(instanceId) {
25779
- ensureRuntimeDirs();
25780
- try {
25781
- updateJsonAtomicSync(ACTIVE_INSTANCE_FILE, (curRaw) => {
25782
- if (!curRaw) return void 0;
25783
- if (instanceId && curRaw.instanceId !== instanceId) return void 0;
25784
- return { ...curRaw, ui_heartbeat_at: Date.now(), updatedAt: Date.now() };
25785
- }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate", timeoutMs: 0 });
25786
- } catch {
25787
- }
25788
- }
25789
- var SERVER_PID_FILE = join10(
25790
- RUNTIME_ROOT,
25791
- `server-${sanitize(resolvePluginData() ?? "default")}.pid`
25792
- );
25793
-
25794
26110
  // src/tui/index.jsx
25795
26111
  var import_mixdog_debug = __toESM(require_mixdog_debug(), 1);
25796
26112
  import { jsx as jsx21 } from "react/jsx-runtime";
25797
- 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";
25798
26114
  var TERMINAL_OSC_RESET_BG = "\x1B]111\x07";
25799
26115
  var TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace("\x1B[?25h", "\x1B[?25l");
25800
- 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}`;
25801
26118
  var BOOT_PROFILE_ENABLED2 = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
25802
26119
  var BOOT_PROFILE_START2 = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance3.now());
25803
26120
  var EXIT_WAIT_TIMEOUT_MS = positiveIntEnv2("MIXDOG_TUI_EXIT_WAIT_MS", 2500);
@@ -25986,8 +26303,8 @@ function waitWithTimeout2(promise, ms) {
25986
26303
  let timer = null;
25987
26304
  return Promise.race([
25988
26305
  Promise.resolve(promise).then(() => true),
25989
- new Promise((resolve6) => {
25990
- timer = setTimeout(() => resolve6(false), ms);
26306
+ new Promise((resolve5) => {
26307
+ timer = setTimeout(() => resolve5(false), ms);
25991
26308
  })
25992
26309
  ]).finally(() => {
25993
26310
  if (timer) clearTimeout(timer);
@@ -26006,7 +26323,7 @@ function scheduleHardExit(code = 0) {
26006
26323
  timer.unref?.();
26007
26324
  }
26008
26325
  function resolveTuiStderrLogPath() {
26009
- 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");
26010
26327
  }
26011
26328
  function ansiFg(rgb) {
26012
26329
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -26088,7 +26405,7 @@ function installTuiStderrGuard() {
26088
26405
  const originalWrite = process.stderr.write.bind(process.stderr);
26089
26406
  const logPath = resolveTuiStderrLogPath();
26090
26407
  try {
26091
- mkdirSync7(dirname9(logPath), { recursive: true });
26408
+ mkdirSync6(dirname9(logPath), { recursive: true });
26092
26409
  } catch {
26093
26410
  }
26094
26411
  (0, import_mixdog_debug.rotateBoundedLog)(logPath, import_mixdog_debug.PLUGIN_LOG_MAX_BYTES, import_mixdog_debug.PLUGIN_LOG_KEEP_BYTES);
@@ -26133,6 +26450,23 @@ function installTuiStderrGuard() {
26133
26450
  }
26134
26451
  };
26135
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
+ }
26136
26470
  async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {}) {
26137
26471
  const startedAt = performance3.now();
26138
26472
  bootProfile2("run:start", { provider, model, toolMode, remote });
@@ -26143,6 +26477,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26143
26477
  return 1;
26144
26478
  }
26145
26479
  const restoreStderr = installTuiStderrGuard();
26480
+ const restoreConsole = installTuiConsoleGuard();
26146
26481
  const stopPerfProbe = installTuiPerfProbe();
26147
26482
  const stopLoopProbe = installTuiLoopProbe();
26148
26483
  const restorePrimedInput = () => drainStdin(process.stdin);
@@ -26183,6 +26518,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26183
26518
  process.off("exit", restoreTerminal);
26184
26519
  } catch {
26185
26520
  }
26521
+ restoreConsole();
26186
26522
  restoreStderr();
26187
26523
  process.stderr.write(`mixdog: ${error?.message || error}
26188
26524
  `);
@@ -26208,20 +26544,10 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26208
26544
  afterCleanup: (reason) => {
26209
26545
  restoreTerminal();
26210
26546
  dumpActiveHandles(`after-${reason}`);
26547
+ restoreConsole();
26211
26548
  restoreStderr();
26212
26549
  }
26213
26550
  });
26214
- const uiHeartbeatTimer = setInterval(() => {
26215
- try {
26216
- touchUiHeartbeat();
26217
- } catch {
26218
- }
26219
- }, 3e4);
26220
- if (typeof uiHeartbeatTimer.unref === "function") uiHeartbeatTimer.unref();
26221
- try {
26222
- touchUiHeartbeat();
26223
- } catch {
26224
- }
26225
26551
  const STDIO_DEATH_FATAL_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED", "EIO"]);
26226
26552
  const stdioDeathListeners = [];
26227
26553
  const registerStdioDeath = (stream, event, { requireCode = false } = {}) => {
@@ -26245,7 +26571,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26245
26571
  registerStdioDeath(process.stderr, "error", { requireCode: true });
26246
26572
  registerStdioDeath(process.stderr, "close");
26247
26573
  try {
26248
- 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() });
26249
26575
  bootProfile2("render:mounted", { ms: (performance3.now() - startedAt).toFixed(1) });
26250
26576
  const { waitUntilExit } = instance;
26251
26577
  if (mouseTracking && typeof instance.setSelection === "function") {
@@ -26263,11 +26589,13 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26263
26589
  if (mouseTracking && typeof instance.getSelectionRows === "function") {
26264
26590
  store.getRenderSelectionRows = instance.getSelectionRows;
26265
26591
  }
26592
+ if (mouseTracking && typeof instance.forceFullRepaint === "function") {
26593
+ store.forceRenderRepaint = instance.forceFullRepaint;
26594
+ }
26266
26595
  await waitUntilExit();
26267
26596
  } finally {
26268
26597
  stopPerfProbe();
26269
26598
  stopLoopProbe();
26270
- clearInterval(uiHeartbeatTimer);
26271
26599
  for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
26272
26600
  try {
26273
26601
  stream.off(event, handler);
@@ -26282,6 +26610,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26282
26610
  }
26283
26611
  restoreTerminal();
26284
26612
  dumpActiveHandles("after-restore");
26613
+ restoreConsole();
26285
26614
  restoreStderr();
26286
26615
  }
26287
26616
  scheduleHardExit(0);