mixdog 0.9.24 → 0.9.26

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 (84) hide show
  1. package/package.json +2 -1
  2. package/scripts/build-runtime-windows.ps1 +242 -242
  3. package/scripts/explore-bench-tmp.mjs +1 -1
  4. package/scripts/recall-usecase-cases.json +1 -1
  5. package/scripts/smoke-runtime-negative.ps1 +106 -106
  6. package/scripts/steering-fold-provenance-test.mjs +71 -0
  7. package/scripts/tool-efficiency-diag.mjs +1 -1
  8. package/scripts/webhook-smoke.mjs +46 -53
  9. package/src/defaults/cycle3-review-prompt.md +11 -4
  10. package/src/defaults/memory-promote-prompt.md +9 -0
  11. package/src/defaults/skills/setup/SKILL.md +6 -1
  12. package/src/rules/lead/02-channels.md +3 -3
  13. package/src/rules/shared/01-tool.md +8 -4
  14. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +17 -0
  15. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/mcp/client.mjs +28 -10
  17. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +8 -1
  18. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
  19. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +51 -15
  20. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +5 -1
  21. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +164 -9
  22. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +45 -0
  23. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -0
  24. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +12 -1
  25. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +2 -2
  26. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +7 -3
  27. package/src/runtime/channels/backends/discord.mjs +10 -3
  28. package/src/runtime/channels/lib/config.mjs +13 -11
  29. package/src/runtime/channels/lib/inbound-handler.mjs +45 -1
  30. package/src/runtime/channels/lib/memory-client.mjs +22 -2
  31. package/src/runtime/channels/lib/owned-runtime.mjs +1 -1
  32. package/src/runtime/channels/lib/scheduler.mjs +226 -208
  33. package/src/runtime/channels/lib/status-snapshot.mjs +16 -0
  34. package/src/runtime/channels/lib/webhook/deliveries.mjs +7 -318
  35. package/src/runtime/channels/lib/webhook.mjs +98 -150
  36. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  37. package/src/runtime/memory/index.mjs +50 -25
  38. package/src/runtime/memory/lib/cycle-scheduler.mjs +20 -2
  39. package/src/runtime/memory/lib/memory-config-flags.mjs +13 -1
  40. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +59 -0
  41. package/src/runtime/memory/lib/memory-cycle2.mjs +10 -3
  42. package/src/runtime/memory/lib/memory-cycle3.mjs +79 -9
  43. package/src/runtime/memory/lib/pg/adapter.mjs +6 -1
  44. package/src/runtime/memory/lib/pg/process.mjs +19 -1
  45. package/src/runtime/memory/lib/pg/supervisor.mjs +125 -14
  46. package/src/runtime/memory/lib/query-handlers.mjs +102 -0
  47. package/src/runtime/memory/lib/recall-format.mjs +60 -22
  48. package/src/runtime/memory/lib/session-ingest-runtime.mjs +20 -6
  49. package/src/runtime/memory/tool-defs.mjs +1 -1
  50. package/src/runtime/shared/child-guardian.mjs +2 -2
  51. package/src/runtime/shared/open-url.mjs +2 -1
  52. package/src/runtime/shared/schedules-db.mjs +350 -0
  53. package/src/runtime/shared/service-discovery.mjs +169 -0
  54. package/src/runtime/shared/spawn-flags.mjs +27 -0
  55. package/src/runtime/shared/tool-primitives.mjs +3 -1
  56. package/src/runtime/shared/tool-surface.mjs +19 -3
  57. package/src/runtime/shared/update-checker.mjs +54 -10
  58. package/src/runtime/shared/webhooks-db.mjs +405 -0
  59. package/src/session-runtime/channel-config-api.mjs +13 -13
  60. package/src/session-runtime/lifecycle-api.mjs +14 -0
  61. package/src/session-runtime/runtime-core.mjs +39 -20
  62. package/src/standalone/agent-tool.mjs +42 -11
  63. package/src/standalone/channel-admin.mjs +173 -121
  64. package/src/standalone/channel-worker.mjs +2 -2
  65. package/src/standalone/memory-runtime-proxy.mjs +10 -5
  66. package/src/tui/App.jsx +32 -10
  67. package/src/tui/app/channel-pickers.mjs +9 -9
  68. package/src/tui/app/clipboard.mjs +14 -0
  69. package/src/tui/app/doctor.mjs +1 -1
  70. package/src/tui/app/maintenance-pickers.mjs +17 -7
  71. package/src/tui/app/settings-picker.mjs +2 -2
  72. package/src/tui/app/transcript-window.mjs +37 -1
  73. package/src/tui/app/use-mouse-input.mjs +19 -6
  74. package/src/tui/components/ToolExecution.jsx +12 -4
  75. package/src/tui/display-width.mjs +10 -4
  76. package/src/tui/dist/index.mjs +166 -58
  77. package/src/tui/engine/agent-job-feed.mjs +42 -3
  78. package/src/tui/engine/session-api-ext.mjs +12 -12
  79. package/src/tui/index.jsx +12 -2
  80. package/src/ui/statusline-segments.mjs +12 -1
  81. package/src/workflows/default/WORKFLOW.md +29 -38
  82. package/src/workflows/solo/WORKFLOW.md +15 -20
  83. package/vendor/ink/build/display-width.js +5 -4
  84. package/src/runtime/shared/schedules-store.mjs +0 -82
@@ -1798,7 +1798,7 @@ function isExternalMcpToolName(name) {
1798
1798
  return isMcpToolName(name) && !isSelfMcpToolName(name);
1799
1799
  }
1800
1800
  function titleCaseMcpServer(server) {
1801
- return String(server || "").split(/[_\s-]+/).filter(Boolean).map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1).toLowerCase()}`).join(" ");
1801
+ return String(server || "").split(/[_\s-]+/).filter(Boolean).map((part) => part === part.toLowerCase() ? `${part.slice(0, 1).toUpperCase()}${part.slice(1)}` : part).join(" ");
1802
1802
  }
1803
1803
  var TOOL_NAME_ALIASES = { tool_search: "load_tool" };
1804
1804
  function normalizeToolName(name) {
@@ -2839,8 +2839,12 @@ function toolWorkUnit(name, args = {}, category = "") {
2839
2839
  case "web_search_call":
2840
2840
  return unitDescriptor("Web Research", { count: queryCount(a, "query", "queries", "keywords") || 1, noun: "query", pluralNoun: "queries" });
2841
2841
  case "web_fetch":
2842
- case "fetch":
2843
2842
  return unitDescriptor("Web Research", { count: queryCount(a, "url", "urls", "uri", "uris") || 1, active: "Fetching", done: "Fetched", noun: "URL", pluralNoun: "URLs" });
2843
+ case "fetch": {
2844
+ const fetchLimit = Number(a.limit ?? a.messages);
2845
+ const fetchCount = Number.isFinite(fetchLimit) && fetchLimit > 0 ? Math.floor(fetchLimit) : queryCount(a, "messages") || 1;
2846
+ return unitDescriptor("Web Research", { count: fetchCount, active: "Fetching", done: "Fetched", noun: "message" });
2847
+ }
2844
2848
  case "recall":
2845
2849
  case "recall_memory":
2846
2850
  case "search_memories":
@@ -2848,8 +2852,14 @@ function toolWorkUnit(name, args = {}, category = "") {
2848
2852
  case "remember":
2849
2853
  case "save_memory":
2850
2854
  case "update_memory":
2851
- case "memory":
2852
2855
  return unitDescriptor("Memory", { count: queryCount(a, "entries", "items", "memories", "query", "text", "value") || 1, active: "Writing", done: "Wrote", noun: "memory item" });
2856
+ case "memory": {
2857
+ const action = String(a.action || "").toLowerCase();
2858
+ const op = String(a.op || "").toLowerCase();
2859
+ const isMutation = op === "add" || op === "edit" || op === "delete" || op === "promote" || op === "dismiss" || action === "core" && !op;
2860
+ if (isMutation) return unitDescriptor("Memory", { count: queryCount(a, "entries", "items", "memories", "query", "text", "value") || 1, active: "Writing", done: "Wrote", noun: "memory item" });
2861
+ return unitDescriptor("Memory", { count: queryCount(a, "entries", "items", "memories", "query", "text", "value") || 1, active: "Checking", done: "Checked", noun: "memory item" });
2862
+ }
2853
2863
  case "explore":
2854
2864
  return unitDescriptor("Explore", { count: queryCount(a, "query", "queries", "prompt", "task", "goal") || 1, noun: "query", pluralNoun: "queries" });
2855
2865
  case "shell":
@@ -2861,8 +2871,11 @@ function toolWorkUnit(name, args = {}, category = "") {
2861
2871
  case "agent":
2862
2872
  case "bridge":
2863
2873
  return unitDescriptor("Agent", { count: queryCount(a, "agents", "roles", "role", "tag", "task_id", "sessionId") || 1, noun: "agent" });
2864
- case "task":
2874
+ case "task": {
2875
+ const action = String(a.action || "").toLowerCase();
2876
+ if (action === "cancel") return unitDescriptor("Task", { count: queryCount(a, "task_id", "task_ids", "id", "ids") || 1, active: "Cancelling", done: "Cancelled", noun: "task" });
2865
2877
  return unitDescriptor("Task", { count: queryCount(a, "task_id", "task_ids", "id", "ids") || 1, noun: "task" });
2878
+ }
2866
2879
  case "skill":
2867
2880
  case "skill_execute":
2868
2881
  case "skill_view":
@@ -4209,11 +4222,11 @@ function isProblemCodePoint(cp) {
4209
4222
  return cp >= 9312 && cp <= 9471 || cp >= 8592 && cp <= 8703;
4210
4223
  }
4211
4224
  var PROBLEM_RE = /[\u2190-\u21ff\u2460-\u24ff]/;
4212
- function resolveAmbiguousWidePolicy(env = process.env) {
4225
+ function resolveAmbiguousWidePolicy(env = process.env, platform = process.platform) {
4213
4226
  const override = env?.MIXDOG_TUI_AMBIGUOUS_WIDE;
4214
4227
  if (override === "1") return true;
4215
4228
  if (override === "0") return false;
4216
- return Boolean(env?.WT_SESSION);
4229
+ return platform === "win32" || Boolean(env?.WT_SESSION);
4217
4230
  }
4218
4231
  var AMBIGUOUS_WIDE = resolveAmbiguousWidePolicy();
4219
4232
  function displayWidthWith(str, wide) {
@@ -7910,8 +7923,15 @@ function osc52ClipboardSequence(text) {
7910
7923
  return `\x1BPtmux;${raw.replaceAll("\x1B", "\x1B\x1B")}\x1B\\`;
7911
7924
  }
7912
7925
  var OSC52_MAX_BYTES = 256 * 1024;
7926
+ function shouldSkipOsc52() {
7927
+ if (process.platform !== "win32") return false;
7928
+ if (process.env.TMUX) return false;
7929
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
7930
+ return true;
7931
+ }
7913
7932
  function writeOsc52Clipboard(text) {
7914
7933
  const value = String(text ?? "");
7934
+ if (shouldSkipOsc52()) return false;
7915
7935
  if (Buffer3.byteLength(value, "utf8") > OSC52_MAX_BYTES) return false;
7916
7936
  try {
7917
7937
  process.stdout.write(osc52ClipboardSequence(value));
@@ -9998,6 +10018,26 @@ function toolArgPathForRows(item) {
9998
10018
  const a = backgroundArgsForRows(item?.args);
9999
10019
  return a?.path ?? a?.file_path ?? a?.file ?? "";
10000
10020
  }
10021
+ function agentCardKeepsCollapsedDetailForRows(item, normalizedName) {
10022
+ if (normalizedName !== "agent") return false;
10023
+ if (toolItemPendingForRows(item)) return false;
10024
+ const bgArgs = backgroundArgsForRows(item.args);
10025
+ const isError = Boolean(item.isError);
10026
+ const hasDisplayResult = toolHasDisplayResultForRows(item);
10027
+ const displayedResultText = toolDisplayedResultTextForRows(item);
10028
+ const rt = item.result == null ? "" : String(item.result).replace(/\s+$/, "");
10029
+ const isAgentResponse = hasDisplayResult && isAgentResponseResultText(rt);
10030
+ const briefRaw = summarizeAgentSurfaceBrief(item.name, bgArgs, displayedResultText, {
10031
+ isError,
10032
+ isResponse: isAgentResponse
10033
+ });
10034
+ const brief = String(briefRaw || "").trim();
10035
+ const status = String(bgArgs.status || "").toLowerCase();
10036
+ const failureText = /\b(cancelled|canceled|failed)\b/i.test(brief) || /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status);
10037
+ const agentHeaderFailure = isError && String(bgArgs.error || "").trim() && !hasDisplayResult;
10038
+ if (agentHeaderFailure && !brief) return false;
10039
+ return isError || failureText;
10040
+ }
10001
10041
  function isShellSurfaceForRows(normalizedName, label = "") {
10002
10042
  const n = String(normalizedName || "").toLowerCase();
10003
10043
  const l = String(label || "").toLowerCase();
@@ -10049,6 +10089,9 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
10049
10089
  if (toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult)) {
10050
10090
  return TOOL_MARGIN_TOP + 1;
10051
10091
  }
10092
+ if (isAgentSurface) {
10093
+ return agentCardKeepsCollapsedDetailForRows(item, normalizedName) ? TOOL_MARGIN_TOP + 1 + 1 : TOOL_MARGIN_TOP + 1;
10094
+ }
10052
10095
  return TOOL_MARGIN_TOP + 1 + 1;
10053
10096
  }
10054
10097
  if (hasRawResult) {
@@ -10056,7 +10099,9 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
10056
10099
  const rawOpts = item.aggregate ? {} : { pathArg: toolArgPathForRows(item), isShell: isShellSurfaceForToolItem(item, normalizedName) };
10057
10100
  return TOOL_MARGIN_TOP + 1 + estimateToolRenderedResultRows(estimateText, { ...rawOpts, columns });
10058
10101
  }
10059
- if (isAgentSurface && !hasResult) return TOOL_MARGIN_TOP + 1 + 1;
10102
+ if (isAgentSurface && !hasResult) {
10103
+ return agentCardKeepsCollapsedDetailForRows(item, normalizedName) ? TOOL_MARGIN_TOP + 1 + 1 : TOOL_MARGIN_TOP + 1;
10104
+ }
10060
10105
  if (isSkillSurface && !hasResult) return TOOL_MARGIN_TOP + 1;
10061
10106
  if (item.aggregate) {
10062
10107
  if (hasRawResult) {
@@ -10437,7 +10482,6 @@ function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24,
10437
10482
  var MOUSE_TRACKING_ON = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
10438
10483
  var MOUSE_TRACKING_OFF = "\x1B[?1006l\x1B[?1002l\x1B[?1000l";
10439
10484
  var ALT_SCROLL_OFF = "\x1B[?1007l";
10440
- var ALT_SCROLL_ON = "\x1B[?1007h";
10441
10485
  var MOUSE_MODIFIER_MASK = 4 | 8 | 16;
10442
10486
  var MOUSE_CTRL_MASK = 16;
10443
10487
  var MOUSE_SHIFT_MASK = 4;
@@ -10478,14 +10522,19 @@ function useMouseInput({
10478
10522
  } catch {
10479
10523
  return;
10480
10524
  }
10481
- if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
10482
- zoomTimerRef.current = setTimeout(() => {
10525
+ const restore = (attempt) => {
10483
10526
  zoomTimerRef.current = null;
10484
10527
  try {
10485
- stdout.write(ALT_SCROLL_ON + MOUSE_TRACKING_ON);
10528
+ stdout.write(MOUSE_TRACKING_ON + ALT_SCROLL_OFF);
10486
10529
  } catch {
10530
+ if (attempt < 5) {
10531
+ zoomTimerRef.current = setTimeout(() => restore(attempt + 1), 200);
10532
+ zoomTimerRef.current.unref?.();
10533
+ }
10487
10534
  }
10488
- }, 700);
10535
+ };
10536
+ if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
10537
+ zoomTimerRef.current = setTimeout(() => restore(0), 700);
10489
10538
  zoomTimerRef.current.unref?.();
10490
10539
  }, [stdout, zoomTimerRef]);
10491
10540
  useEffect6(() => () => {
@@ -13348,13 +13397,14 @@ function createMaintenancePickers({
13348
13397
  const upd = readSettings();
13349
13398
  const status = readStatus();
13350
13399
  const current = upd.currentVersion || "unknown";
13400
+ const installedVersion = status.phase === "installed" ? status.version || upd.latestVersion || null : null;
13351
13401
  const latestMeta = checking || status.phase === "checking" ? "checking\u2026" : upd.latestVersion || "unknown";
13352
13402
  const items = [
13353
13403
  {
13354
13404
  value: "current",
13355
13405
  label: "Current version",
13356
- meta: current,
13357
- description: "Installed mixdog version.",
13406
+ meta: installedVersion ? `${current} \u2192 ${installedVersion}` : current,
13407
+ description: installedVersion ? `v${installedVersion} installed \u2014 restart mixdog to apply.` : "Installed mixdog version.",
13358
13408
  _action: "current"
13359
13409
  },
13360
13410
  {
@@ -13388,11 +13438,11 @@ function createMaintenancePickers({
13388
13438
  buttons: [
13389
13439
  {
13390
13440
  value: "update-now",
13391
- label: upd.updateAvailable ? `Update to v${upd.latestVersion || "latest"}` : "Update now"
13441
+ label: installedVersion ? `v${installedVersion} installed \u2014 restart to apply` : status.phase === "installing" ? "Installing\u2026" : upd.updateAvailable ? `Update to v${upd.latestVersion || "latest"}` : "Update now"
13392
13442
  }
13393
13443
  ],
13394
13444
  onConfirm: (button) => {
13395
- if (button?.value === "update-now") runUpdate();
13445
+ if (button?.value === "update-now" && !installedVersion && status.phase !== "installing") runUpdate();
13396
13446
  }
13397
13447
  },
13398
13448
  onSelect: (_value, item) => {
@@ -13428,7 +13478,7 @@ function createMaintenancePickers({
13428
13478
  store.pushNotice("Updating\u2026", "info");
13429
13479
  void Promise.resolve(store.runUpdateNow?.()).then((result) => {
13430
13480
  if (result?.ok) {
13431
- store.pushNotice(`v${result.version} installed \u2014 restart to apply`, "info");
13481
+ store.pushNotice(`v${result.version} installed \u2014 restart to apply`, "warn");
13432
13482
  } else {
13433
13483
  store.pushNotice(`Update failed: ${result?.error || "unknown error"}`, "error");
13434
13484
  }
@@ -14958,7 +15008,7 @@ function createChannelPickers({
14958
15008
  onboardingWarnReopen
14959
15009
  }) {
14960
15010
  let pickerGen = 0;
14961
- const openChannelTypeActionsPicker = (backend, options = {}) => {
15011
+ const openChannelTypeActionsPicker = async (backend, options = {}) => {
14962
15012
  pickerGen += 1;
14963
15013
  const parentReturn = typeof options.returnTo === "function" ? options.returnTo : () => openChannelSettingTypePicker();
14964
15014
  setProviderPrompt(null);
@@ -14967,7 +15017,7 @@ function createChannelPickers({
14967
15017
  setContextPanel(null);
14968
15018
  let setup;
14969
15019
  try {
14970
- setup = store.getChannelSetup();
15020
+ setup = await store.getChannelSetup();
14971
15021
  } catch (e) {
14972
15022
  store.pushNotice(`channels failed: ${e?.message || e}`, "error");
14973
15023
  return;
@@ -15041,7 +15091,7 @@ function createChannelPickers({
15041
15091
  }
15042
15092
  });
15043
15093
  };
15044
- const openChannelSettingTypePicker = (options = {}) => {
15094
+ const openChannelSettingTypePicker = async (options = {}) => {
15045
15095
  pickerGen += 1;
15046
15096
  const returnTo = typeof options.returnTo === "function" ? options.returnTo : () => {
15047
15097
  };
@@ -15052,7 +15102,7 @@ function createChannelPickers({
15052
15102
  setContextPanel(null);
15053
15103
  let setup;
15054
15104
  try {
15055
- setup = store.getChannelSetup();
15105
+ setup = await store.getChannelSetup();
15056
15106
  } catch (e) {
15057
15107
  store.pushNotice(`channels failed: ${e?.message || e}`, "error");
15058
15108
  return;
@@ -15115,7 +15165,7 @@ function createChannelPickers({
15115
15165
  setContextPanel(null);
15116
15166
  let setup;
15117
15167
  try {
15118
- setup = store.getChannelSetup();
15168
+ setup = await store.getChannelSetup();
15119
15169
  } catch (e) {
15120
15170
  store.pushNotice(`channels failed: ${e?.message || e}`, "error");
15121
15171
  return;
@@ -15148,14 +15198,14 @@ function createChannelPickers({
15148
15198
  _action: "noop"
15149
15199
  }]
15150
15200
  ];
15151
- const toggleSchedule = (item) => {
15201
+ const toggleSchedule = async (item) => {
15152
15202
  if (item._action !== "schedule-toggle") return;
15153
15203
  if (!channelRemoteEnabled) {
15154
15204
  store.pushNotice("enable channel first", "warn");
15155
15205
  return;
15156
15206
  }
15157
15207
  try {
15158
- store.setScheduleEnabled?.(item._name, !item._enabled);
15208
+ await store.setScheduleEnabled?.(item._name, !item._enabled);
15159
15209
  void openChannelSetupPicker("schedules", { highlightValue: `schedule:${item._name}` });
15160
15210
  } catch (e) {
15161
15211
  store.pushNotice(`schedule toggle failed: ${e?.message || e}`, "error");
@@ -15251,14 +15301,14 @@ function createChannelPickers({
15251
15301
  _action: "noop"
15252
15302
  }]
15253
15303
  ];
15254
- const toggleWebhook = (item) => {
15304
+ const toggleWebhook = async (item) => {
15255
15305
  if (item._action !== "webhook-toggle") return;
15256
15306
  if (!channelRemoteEnabled) {
15257
15307
  store.pushNotice("enable channel first", "warn");
15258
15308
  return;
15259
15309
  }
15260
15310
  try {
15261
- store.setWebhookEnabled?.(item._name, !item._enabled);
15311
+ await store.setWebhookEnabled?.(item._name, !item._enabled);
15262
15312
  void openChannelSetupPicker("webhooks", { highlightValue: `webhook:${item._name}` });
15263
15313
  } catch (e) {
15264
15314
  store.pushNotice(`webhook toggle failed: ${e?.message || e}`, "error");
@@ -16722,7 +16772,7 @@ function createSettingsPicker({
16722
16772
  openUpdatePicker,
16723
16773
  openChannelSettingTypePicker
16724
16774
  }) {
16725
- const openSettingsPicker = (opts = {}) => {
16775
+ const openSettingsPicker = async (opts = {}) => {
16726
16776
  const light = opts.light === true;
16727
16777
  const overrides = opts.overrides || null;
16728
16778
  const heavyCache = light ? settingsHeavyCacheRef.current : null;
@@ -16742,7 +16792,7 @@ function createSettingsPicker({
16742
16792
  channelBackend = heavyCache.channelBackend || "discord";
16743
16793
  } else {
16744
16794
  try {
16745
- channelBackend = store.getChannelSetup?.()?.backend || "discord";
16795
+ channelBackend = (await store.getChannelSetup?.())?.backend || "discord";
16746
16796
  } catch {
16747
16797
  channelBackend = "discord";
16748
16798
  }
@@ -18672,7 +18722,7 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18672
18722
  }, [pending, pendingDisplayReady, startedAtMs]);
18673
18723
  if (pending && !pendingDisplayReady) {
18674
18724
  const placeholderNormalizedName = String(formatToolSurface(name, args)?.normalizedName || "").toLowerCase();
18675
- const placeholderSingleRow = !aggregate && SKILL_SURFACE_NAMES2.has(placeholderNormalizedName);
18725
+ const placeholderSingleRow = !aggregate && (SKILL_SURFACE_NAMES2.has(placeholderNormalizedName) || isAgentTool(placeholderNormalizedName));
18676
18726
  return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", marginTop: attached ? 0 : 1, width: rowWidth, overflow: "hidden", children: [
18677
18727
  /* @__PURE__ */ jsx16(Text16, { children: " " }),
18678
18728
  placeholderSingleRow ? null : /* @__PURE__ */ jsx16(Text16, { children: " " })
@@ -18784,7 +18834,9 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18784
18834
  } else if (isAgentSurfaceCard && !showRawResult) {
18785
18835
  const agentDetailFallback = collapsedDetail || (pending ? pendingDetailPlaceholder || "Running" : "Finished");
18786
18836
  const agentDetailLine = agentSurfaceBrief || truncateToWidth(String(agentDetailFallback), Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars));
18787
- visibleDetailLines = agentHeaderFailure && !agentSurfaceBrief ? [] : [agentDetailLine];
18837
+ const agentFailureText = /\b(Cancelled|Canceled|Failed)\b/i.test(agentSurfaceBrief || collapsedDetail || "");
18838
+ const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
18839
+ visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
18788
18840
  }
18789
18841
  const finalStatusColor = toolStatusColor({ pending, groupCount, failedCount, terminalStatus });
18790
18842
  const dotColor = finalStatusColor;
@@ -19932,7 +19984,9 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
19932
19984
  copySelection();
19933
19985
  return;
19934
19986
  }
19935
- showSelectionCopyHint("select text to copy \xB7 Esc interrupts", "plain");
19987
+ if (process.platform !== "win32") {
19988
+ showSelectionCopyHint("select text to copy \xB7 Esc interrupts", "plain");
19989
+ }
19936
19990
  return;
19937
19991
  }
19938
19992
  if (key.ctrl && (input === "o" || input === "O")) {
@@ -20398,19 +20452,25 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20398
20452
  }
20399
20453
  if (channelPrompt.kind === "schedule-add") {
20400
20454
  const [name, time, instructions, channel, model] = parts;
20401
- store.saveSchedule({ name, time, instructions, channel, model });
20402
- setChannelPrompt(null);
20403
- void openChannelSetupPicker("schedules");
20455
+ Promise.resolve(store.saveSchedule({ name, time, instructions, channel, model })).then(() => {
20456
+ setChannelPrompt(null);
20457
+ void openChannelSetupPicker("schedules");
20458
+ }).catch((e) => {
20459
+ store.pushNotice(`schedule save failed: ${e?.message || e}`, "error");
20460
+ });
20404
20461
  return true;
20405
20462
  }
20406
20463
  if (channelPrompt.kind === "webhook-add") {
20407
20464
  const [name, instructions, channel, model, parser] = parts;
20408
- const result = store.saveWebhook({ name, instructions, channel, model, parser });
20409
- if (result?.secret) {
20410
- store.pushNotice(`webhook secret for ${result.name}: ${result.secret}`, "info");
20411
- }
20412
- setChannelPrompt(null);
20413
- void openChannelSetupPicker("webhooks");
20465
+ Promise.resolve(store.saveWebhook({ name, instructions, channel, model, parser })).then((result) => {
20466
+ if (result?.secret) {
20467
+ store.pushNotice(`webhook secret for ${result.name}: ${result.secret}`, "info");
20468
+ }
20469
+ setChannelPrompt(null);
20470
+ void openChannelSetupPicker("webhooks");
20471
+ }).catch((e) => {
20472
+ store.pushNotice(`webhook save failed: ${e?.message || e}`, "error");
20473
+ });
20414
20474
  return true;
20415
20475
  }
20416
20476
  } catch (e) {
@@ -22810,6 +22870,15 @@ function createToolCardResults({
22810
22870
  }
22811
22871
 
22812
22872
  // src/tui/engine/agent-job-feed.mjs
22873
+ function parseInboundImagePaths(raw) {
22874
+ if (typeof raw !== "string" || !raw) return [];
22875
+ try {
22876
+ const arr = JSON.parse(raw);
22877
+ return Array.isArray(arr) ? arr.filter((p) => typeof p === "string" && p.length > 0) : [];
22878
+ } catch {
22879
+ return [];
22880
+ }
22881
+ }
22813
22882
  function createAgentJobFeed({
22814
22883
  runtime,
22815
22884
  getState,
@@ -22896,13 +22965,38 @@ function createAgentJobFeed({
22896
22965
  set(agentStatusState({ force: true }));
22897
22966
  }
22898
22967
  const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
22899
- if (!modelContent) return true;
22900
- enqueue(modelContent, {
22968
+ const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
22969
+ if (!modelContent && imagePaths.length === 0) return true;
22970
+ const enqueueOpts = {
22901
22971
  mode: "task-notification",
22902
22972
  priority: "next",
22903
22973
  key: notificationKey || void 0,
22904
22974
  displayText: delivery.displayText || text
22905
- });
22975
+ };
22976
+ if (imagePaths.length > 0) {
22977
+ void (async () => {
22978
+ if (getDisposed()) return;
22979
+ const parts = [];
22980
+ if (modelContent) parts.push({ type: "text", text: modelContent });
22981
+ for (const p of imagePaths) {
22982
+ let att = null;
22983
+ try {
22984
+ att = await readImageAttachmentFromPath(p);
22985
+ } catch {
22986
+ att = null;
22987
+ }
22988
+ if (!att) continue;
22989
+ if (att.metadataText) parts.push({ type: "text", text: att.metadataText });
22990
+ parts.push({ type: "image", data: att.content, mimeType: att.mediaType || "image/png" });
22991
+ }
22992
+ if (getDisposed()) return;
22993
+ const hasImage = parts.some((part) => part.type === "image");
22994
+ if (!hasImage && !modelContent) return;
22995
+ enqueue(hasImage ? parts : modelContent, enqueueOpts);
22996
+ })();
22997
+ return true;
22998
+ }
22999
+ enqueue(modelContent, enqueueOpts);
22906
23000
  return true;
22907
23001
  });
22908
23002
  }
@@ -24757,33 +24851,33 @@ function createEngineApiB(bag) {
24757
24851
  pushNotice("webhook config updated", "info");
24758
24852
  return result;
24759
24853
  },
24760
- saveSchedule: (entry) => {
24761
- const result = runtime.saveSchedule(entry);
24854
+ saveSchedule: async (entry) => {
24855
+ const result = await runtime.saveSchedule(entry);
24762
24856
  pushNotice(`schedule saved: ${result.name}`, "info");
24763
24857
  return result;
24764
24858
  },
24765
- deleteSchedule: (name) => {
24766
- const result = runtime.deleteSchedule(name);
24859
+ deleteSchedule: async (name) => {
24860
+ const result = await runtime.deleteSchedule(name);
24767
24861
  pushNotice(`schedule deleted: ${name}`, "info");
24768
24862
  return result;
24769
24863
  },
24770
- setScheduleEnabled: (name, enabled) => {
24771
- const result = runtime.setScheduleEnabled(name, enabled);
24864
+ setScheduleEnabled: async (name, enabled) => {
24865
+ const result = await runtime.setScheduleEnabled(name, enabled);
24772
24866
  pushNotice(`schedule ${enabled ? "enabled" : "disabled"}: ${name}`, "info");
24773
24867
  return result;
24774
24868
  },
24775
- saveWebhook: (entry) => {
24776
- const result = runtime.saveWebhook(entry);
24869
+ saveWebhook: async (entry) => {
24870
+ const result = await runtime.saveWebhook(entry);
24777
24871
  pushNotice(`webhook saved: ${result.name}`, "info");
24778
24872
  return result;
24779
24873
  },
24780
- deleteWebhook: (name) => {
24781
- const result = runtime.deleteWebhook(name);
24874
+ deleteWebhook: async (name) => {
24875
+ const result = await runtime.deleteWebhook(name);
24782
24876
  pushNotice(`webhook deleted: ${name}`, "info");
24783
24877
  return result;
24784
24878
  },
24785
- setWebhookEnabled: (name, enabled) => {
24786
- const result = runtime.setWebhookEnabled(name, enabled);
24879
+ setWebhookEnabled: async (name, enabled) => {
24880
+ const result = await runtime.setWebhookEnabled(name, enabled);
24787
24881
  pushNotice(`webhook ${enabled ? "enabled" : "disabled"}: ${name}`, "info");
24788
24882
  return result;
24789
24883
  },
@@ -24961,11 +25055,24 @@ function createEngineApiB(bag) {
24961
25055
  // src/runtime/shared/update-checker.mjs
24962
25056
  import { dirname as dirname7, join as join8 } from "node:path";
24963
25057
  import { fileURLToPath as fileURLToPath2 } from "node:url";
25058
+
25059
+ // src/runtime/shared/spawn-flags.mjs
25060
+ var isWin = process.platform === "win32";
25061
+ var detachedSpawnOpts = Object.freeze({
25062
+ windowsHide: true,
25063
+ ...isWin ? {} : { detached: true }
25064
+ });
25065
+ var hiddenSpawnOpts = Object.freeze({
25066
+ windowsHide: true
25067
+ });
25068
+
25069
+ // src/runtime/shared/update-checker.mjs
24964
25070
  var PACKAGE_NAME = "mixdog";
24965
25071
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
24966
25072
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
24967
25073
  var _MODULE_DIR = dirname7(fileURLToPath2(import.meta.url));
24968
25074
  var _PACKAGE_JSON_PATH = join8(_MODULE_DIR, "..", "..", "..", "package.json");
25075
+ var _PACKAGE_ROOT = dirname7(_PACKAGE_JSON_PATH);
24969
25076
  function parseSemver(value) {
24970
25077
  const text = String(value || "").trim().replace(/^v/i, "");
24971
25078
  const [core, ...preParts] = text.split("-");
@@ -25112,7 +25219,7 @@ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
25112
25219
  return;
25113
25220
  }
25114
25221
  const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
25115
- const setup = runtime.getChannelSetup?.() || {};
25222
+ const setup = await runtime.getChannelSetup?.() || {};
25116
25223
  const tokens = [];
25117
25224
  if (setup.discord?.authenticated) tokens.push("discord");
25118
25225
  if (setup.telegram?.authenticated) tokens.push("telegram");
@@ -26279,7 +26386,8 @@ var TERMINAL_MODE_RESET = "\x1B[?1006l\x1B[?1005l\x1B[?1015l\x1B[?1003l\x1B[?100
26279
26386
  var TERMINAL_OSC_RESET_BG = "\x1B]111\x07";
26280
26387
  var TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace("\x1B[?25h", "\x1B[?25l");
26281
26388
  var XTSHIFTESCAPE_ON = process.env.WT_SESSION ? "" : "\x1B[>1s";
26282
- var MOUSE_TRACKING_ON2 = `\x1B[?1000h\x1B[?1002h\x1B[?1006h${XTSHIFTESCAPE_ON}`;
26389
+ var MOUSE_TRACKING_ON2 = `\x1B[?1000h\x1B[?1002h\x1B[?1006h\x1B[?1007l${XTSHIFTESCAPE_ON}`;
26390
+ var ALT_SCROLL_RESTORE = "\x1B[?1007h";
26283
26391
  var BOOT_PROFILE_ENABLED2 = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
26284
26392
  var BOOT_PROFILE_START2 = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance3.now());
26285
26393
  var EXIT_WAIT_TIMEOUT_MS = positiveIntEnv2("MIXDOG_TUI_EXIT_WAIT_MS", 2500);
@@ -26655,7 +26763,7 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
26655
26763
  process.stdout.write(
26656
26764
  // Pop kitty + disable modifyOtherKeys BEFORE leaving the alt screen.
26657
26765
  // Both are no-ops if nothing was enabled, so this is always safe.
26658
- `${TERMINAL_MODE_RESET}\x1B[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1B[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`
26766
+ `${TERMINAL_MODE_RESET}${ALT_SCROLL_RESTORE}\x1B[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1B[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`
26659
26767
  );
26660
26768
  } catch {
26661
26769
  }
@@ -23,6 +23,20 @@ import {
23
23
  notificationQueueKey,
24
24
  resolveTuiRuntimeNotificationDelivery,
25
25
  } from './notification-plan.mjs';
26
+ import { readImageAttachmentFromPath } from '../paste-attachments.mjs';
27
+
28
+ // Channel inbound images arrive as a JSON-array-of-paths meta value (stringified
29
+ // across the notify IPC boundary). Parse defensively; a malformed value simply
30
+ // yields no images and the notification degrades to its text body.
31
+ function parseInboundImagePaths(raw) {
32
+ if (typeof raw !== 'string' || !raw) return [];
33
+ try {
34
+ const arr = JSON.parse(raw);
35
+ return Array.isArray(arr) ? arr.filter((p) => typeof p === 'string' && p.length > 0) : [];
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
26
40
 
27
41
  export function createAgentJobFeed({
28
42
  runtime,
@@ -117,13 +131,38 @@ export function createAgentJobFeed({
117
131
  set(agentStatusState({ force: true }));
118
132
  }
119
133
  const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
120
- if (!modelContent) return true;
121
- enqueue(modelContent, {
134
+ const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
135
+ if (!modelContent && imagePaths.length === 0) return true;
136
+ const enqueueOpts = {
122
137
  mode: 'task-notification',
123
138
  priority: 'next',
124
139
  key: notificationKey || undefined,
125
140
  displayText: delivery.displayText || text,
126
- });
141
+ };
142
+ if (imagePaths.length > 0) {
143
+ // Read each downloaded image into a real image content block so the
144
+ // channel turn is vision-visible. Async, but the notification is
145
+ // already "handled" (return true) — the enqueue lands on resolve.
146
+ // Any unreadable path is skipped; if none load, fall back to text.
147
+ void (async () => {
148
+ if (getDisposed()) return;
149
+ const parts = [];
150
+ if (modelContent) parts.push({ type: 'text', text: modelContent });
151
+ for (const p of imagePaths) {
152
+ let att = null;
153
+ try { att = await readImageAttachmentFromPath(p); } catch { att = null; }
154
+ if (!att) continue;
155
+ if (att.metadataText) parts.push({ type: 'text', text: att.metadataText });
156
+ parts.push({ type: 'image', data: att.content, mimeType: att.mediaType || 'image/png' });
157
+ }
158
+ if (getDisposed()) return;
159
+ const hasImage = parts.some((part) => part.type === 'image');
160
+ if (!hasImage && !modelContent) return;
161
+ enqueue(hasImage ? parts : modelContent, enqueueOpts);
162
+ })();
163
+ return true;
164
+ }
165
+ enqueue(modelContent, enqueueOpts);
127
166
  return true;
128
167
  });
129
168
  }
@@ -279,33 +279,33 @@ export function createEngineApiB(bag) {
279
279
  pushNotice('webhook config updated', 'info');
280
280
  return result;
281
281
  },
282
- saveSchedule: (entry) => {
283
- const result = runtime.saveSchedule(entry);
282
+ saveSchedule: async (entry) => {
283
+ const result = await runtime.saveSchedule(entry);
284
284
  pushNotice(`schedule saved: ${result.name}`, 'info');
285
285
  return result;
286
286
  },
287
- deleteSchedule: (name) => {
288
- const result = runtime.deleteSchedule(name);
287
+ deleteSchedule: async (name) => {
288
+ const result = await runtime.deleteSchedule(name);
289
289
  pushNotice(`schedule deleted: ${name}`, 'info');
290
290
  return result;
291
291
  },
292
- setScheduleEnabled: (name, enabled) => {
293
- const result = runtime.setScheduleEnabled(name, enabled);
292
+ setScheduleEnabled: async (name, enabled) => {
293
+ const result = await runtime.setScheduleEnabled(name, enabled);
294
294
  pushNotice(`schedule ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
295
295
  return result;
296
296
  },
297
- saveWebhook: (entry) => {
298
- const result = runtime.saveWebhook(entry);
297
+ saveWebhook: async (entry) => {
298
+ const result = await runtime.saveWebhook(entry);
299
299
  pushNotice(`webhook saved: ${result.name}`, 'info');
300
300
  return result;
301
301
  },
302
- deleteWebhook: (name) => {
303
- const result = runtime.deleteWebhook(name);
302
+ deleteWebhook: async (name) => {
303
+ const result = await runtime.deleteWebhook(name);
304
304
  pushNotice(`webhook deleted: ${name}`, 'info');
305
305
  return result;
306
306
  },
307
- setWebhookEnabled: (name, enabled) => {
308
- const result = runtime.setWebhookEnabled(name, enabled);
307
+ setWebhookEnabled: async (name, enabled) => {
308
+ const result = await runtime.setWebhookEnabled(name, enabled);
309
309
  pushNotice(`webhook ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
310
310
  return result;
311
311
  },
package/src/tui/index.jsx CHANGED
@@ -33,7 +33,17 @@ const TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace('\x1b[?25h
33
33
  // highlight, native copy); ctrl+click/right-click remain the app-side
34
34
  // extend triggers there. Restored via `\x1b[>0s` in TERMINAL_MODE_RESET.
35
35
  const XTSHIFTESCAPE_ON = process.env.WT_SESSION ? '' : '\x1b[>1s';
36
- const MOUSE_TRACKING_ON = `\x1b[?1000h\x1b[?1002h\x1b[?1006h${XTSHIFTESCAPE_ON}`;
36
+ // Trailing `\x1b[?1007l` keeps alternate-scroll OFF for the whole session:
37
+ // while mouse tracking is on it is irrelevant (wheel arrives as SGR events),
38
+ // but if tracking ever drops (failed re-enable after the ctrl+wheel zoom
39
+ // passthrough, terminal hiccup), an enabled 1007 makes the terminal convert
40
+ // wheel input into Up/Down arrows — which lands in PROMPT HISTORY instead of
41
+ // transcript scroll. With 1007 forced off the wheel degrades to a no-op, never
42
+ // to history navigation. Restored to on (`\x1b[?1007h`) at teardown so
43
+ // alt-screen apps that rely on alternate scroll (less/vim under Windows
44
+ // Terminal, whose default is on) keep working after mixdog exits.
45
+ const MOUSE_TRACKING_ON = `\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1007l${XTSHIFTESCAPE_ON}`;
46
+ const ALT_SCROLL_RESTORE = '\x1b[?1007h';
37
47
  // Keyboard-protocol teardown. App.jsx enables kitty + modifyOtherKeys
38
48
  // synchronously at raw-mode-on (no query); here we just pop/disable them on
39
49
  // exit. POP_KITTY / DISABLE_MODIFY_OTHER_KEYS come from keyboard-protocol.mjs.
@@ -434,7 +444,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
434
444
  process.stdout.write(
435
445
  // Pop kitty + disable modifyOtherKeys BEFORE leaving the alt screen.
436
446
  // Both are no-ops if nothing was enabled, so this is always safe.
437
- `${TERMINAL_MODE_RESET}\x1b[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1b[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`,
447
+ `${TERMINAL_MODE_RESET}${ALT_SCROLL_RESTORE}\x1b[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1b[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`,
438
448
  );
439
449
  } catch { /* ignore */ }
440
450
  };