mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -1,6 +1,41 @@
1
1
  import sliceAnsi from 'slice-ansi';
2
- import stringWidth from 'string-width';
3
2
  import { styledCharsFromTokens, styledCharsToString, tokenize, } from '@alcalzone/ansi-tokenize';
3
+ // [mixdog fork] use the shared display-width policy so ink's per-character
4
+ // advance + width cache treat circled digits / arrows as 2 cells under Windows
5
+ // Terminal, matching OUR wrap/row math. See display-width.js (kept in sync with
6
+ // src/tui/display-width.mjs).
7
+ import { displayStringWidth as stringWidth } from './display-width.js';
8
+ const RGB_COLOR_RE = /^rgb\(\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\s*\)$/;
9
+ const rgbStyle = (value, type, fallback) => {
10
+ const match = RGB_COLOR_RE.exec(String(value || ''));
11
+ const parts = match
12
+ ? match.slice(1, 4).map(part => Math.max(0, Math.min(255, Number(part) || 0)))
13
+ : fallback;
14
+ const sgr = type === 'foreground' ? 38 : 48;
15
+ const reset = type === 'foreground' ? '\x1b[39m' : '\x1b[49m';
16
+ return { code: `\x1b[${sgr};2;${parts[0]};${parts[1]};${parts[2]}m`, endCode: reset };
17
+ };
18
+ const intersectClips = (clips) => {
19
+ if (clips.length === 0) {
20
+ return undefined;
21
+ }
22
+ const out = {};
23
+ for (const clip of clips) {
24
+ if (typeof clip?.x1 === 'number') {
25
+ out.x1 = typeof out.x1 === 'number' ? Math.max(out.x1, clip.x1) : clip.x1;
26
+ }
27
+ if (typeof clip?.x2 === 'number') {
28
+ out.x2 = typeof out.x2 === 'number' ? Math.min(out.x2, clip.x2) : clip.x2;
29
+ }
30
+ if (typeof clip?.y1 === 'number') {
31
+ out.y1 = typeof out.y1 === 'number' ? Math.max(out.y1, clip.y1) : clip.y1;
32
+ }
33
+ if (typeof clip?.y2 === 'number') {
34
+ out.y2 = typeof out.y2 === 'number' ? Math.min(out.y2, clip.y2) : clip.y2;
35
+ }
36
+ }
37
+ return out;
38
+ };
4
39
  class OutputCaches {
5
40
  widths = new Map();
6
41
  blockWidths = new Map();
@@ -112,10 +147,19 @@ export default class Output {
112
147
  const { text, transformers } = operation;
113
148
  let { x, y } = operation;
114
149
  let lines = text.split('\n');
115
- const clip = clips.at(-1);
150
+ // [mixdog fork] Nested overflow:hidden boxes must be clipped to
151
+ // the INTERSECTION of every active ancestor clip. Using only the
152
+ // innermost clip lets child components such as ToolExecution /
153
+ // TurnDone (which have their own overflow hidden rows) escape the
154
+ // transcript viewport and paint over command/prompt/status rows.
155
+ const clip = intersectClips(clips);
116
156
  if (clip) {
117
157
  const clipHorizontally = typeof clip?.x1 === 'number' && typeof clip?.x2 === 'number';
118
158
  const clipVertically = typeof clip?.y1 === 'number' && typeof clip?.y2 === 'number';
159
+ if ((clipHorizontally && clip.x2 <= clip.x1) ||
160
+ (clipVertically && clip.y2 <= clip.y1)) {
161
+ continue;
162
+ }
119
163
  // If text is positioned outside of clipping area altogether,
120
164
  // skip to the next operation to avoid unnecessary calculations
121
165
  if (clipHorizontally) {
@@ -218,15 +262,34 @@ export default class Output {
218
262
  // dim text, and status colors do not bleed through.
219
263
  const sel = this.selection;
220
264
  let selectedText = null;
265
+ // [mixdog fork] noSelect exclusion (claude-code skips gutter / line-number
266
+ // / diff-sigil cells from both highlight and copy via screen.noSelect):
267
+ // mixdog's cell model has NO noSelect marker — the Output grid stores only
268
+ // {value, styles} per cell, with no flag distinguishing gutter cells from
269
+ // content. Inferring gutters from position/content would be a fragile
270
+ // heuristic (line numbers, diff +/- sigils, and real content are
271
+ // indistinguishable at the cell level), so this is deliberately NOT
272
+ // implemented. It needs a noSelect bit threaded through the render
273
+ // pipeline before it can be done cleanly.
221
274
  if (sel) {
222
275
  const captureSelectedText = sel.captureText !== false;
223
276
  if (!captureSelectedText) {
224
277
  selectedText = undefined;
225
278
  }
226
- const selectionStyles = [
227
- { code: '', endCode: '' },
228
- { code: '', endCode: '' },
229
- ];
279
+ // [mixdog fork] Port applySelectionOverlay's fg-preserving principle
280
+ // from claude-code's selection.ts: REPLACE only the background and
281
+ // PRESERVE each cell's own foreground/attribute styles, so syntax
282
+ // highlighting, OSC-8 links, and dim text stay readable under the
283
+ // selection. The old code forced a black fg + light bg, flattening
284
+ // every colored glyph to one color. selectionForeground is now
285
+ // intentionally unused (kept in the setter API for compatibility).
286
+ const selectionBg = rgbStyle(sel.selectionBackground, 'background', [245, 245, 245]);
287
+ // A style entry sets the background when its opening SGR code begins
288
+ // with a background parameter: 40-47 (basic), 48 (256/truecolor),
289
+ // 49 (default), or 100-107 (bright). ansi-tokenize emits one entry
290
+ // per SGR attribute, so a prefix test on `.code` cleanly isolates the
291
+ // bg-only entries we want to drop before appending the selection bg.
292
+ const isBackgroundStyle = (s) => typeof s?.code === 'string' && /^\x1b\[(?:4[0-9]|10[0-7])[;m]/.test(s.code);
230
293
  const linear = sel.mode === 'linear';
231
294
  const start = linear && (sel.y1 > sel.y2 || (sel.y1 === sel.y2 && sel.x1 > sel.x2))
232
295
  ? { x: sel.x2, y: sel.y2 }
@@ -281,9 +344,16 @@ export default class Output {
281
344
  if (contentStart === -1 || x < contentStart || x > contentEnd) {
282
345
  continue;
283
346
  }
347
+ // [mixdog fork] fg-preserving highlight: keep the cell's own
348
+ // style entries (foreground color, bold/dim/underline, link
349
+ // attrs) and swap ONLY the background. Drop any existing
350
+ // background entry so the two bgs don't fight, then append the
351
+ // selection bg last so it wins for this cell.
352
+ const baseStyles = Array.isArray(cell.styles) ? cell.styles : [];
353
+ const preserved = baseStyles.filter(style => !isBackgroundStyle(style));
284
354
  row[x] = {
285
355
  ...cell,
286
- styles: selectionStyles,
356
+ styles: [...preserved, selectionBg],
287
357
  };
288
358
  }
289
359
  // Trailing spaces in a selected row are padding, not content.
@@ -302,6 +372,20 @@ export default class Output {
302
372
  while (selRows.length > 0 && selRows[selRows.length - 1].trim() === '') {
303
373
  selRows.pop();
304
374
  }
375
+ // [mixdog fork] SOFT-WRAP JOIN (claude-code getSelectedText):
376
+ // claude-code rejoins word-wrapped continuation rows into one
377
+ // logical line using screen.softWrap, a per-row bitmap set at
378
+ // wrap time marking a row as a continuation of the one above.
379
+ // mixdog's rect(linear) render model carries NO softWrap metadata
380
+ // — the Output grid is a flat array of visual rows with no record
381
+ // of which line breaks were inserted by wrapping vs. present in
382
+ // the source. Detecting "this row filled to content width AND the
383
+ // next continues the same logical block" from cell values alone is
384
+ // a guess (a source line that legitimately fills the width would be
385
+ // wrongly glued to the next). Per the correctness-over-cleverness
386
+ // rule we keep the honest per-visual-row '\n' join rather than
387
+ // fabricate wrap boundaries. Revisit if the render pipeline starts
388
+ // emitting a softWrap bit per row.
305
389
  selectedText = selRows.join('\n');
306
390
  }
307
391
  }
@@ -317,8 +401,30 @@ export default class Output {
317
401
  const generatedOutput = output
318
402
  .map(line => {
319
403
  // See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742
320
- const lineWithoutEmptyItems = line.filter(item => item !== undefined);
321
- return styledCharsToString(lineWithoutEmptyItems).trimEnd();
404
+ // [mixdog fork] Keep the serialized row inside Output.width as a
405
+ // final invariant. Component-level width budgeting can miss
406
+ // background fills, resize races, or wide-char edge cases; if the
407
+ // terminal receives a row wider than its modeled width it may
408
+ // soft-wrap/scroll outside Ink's cursor-origin model.
409
+ const lineWithinWidth = [];
410
+ let usedWidth = 0;
411
+ for (const item of line) {
412
+ if (item === undefined) {
413
+ continue;
414
+ }
415
+ const value = item.value ?? '';
416
+ if (value === '') {
417
+ lineWithinWidth.push(item);
418
+ continue;
419
+ }
420
+ const itemWidth = Math.max(1, this.caches.getStringWidth(value));
421
+ if (usedWidth + itemWidth > this.width) {
422
+ break;
423
+ }
424
+ lineWithinWidth.push(item);
425
+ usedWidth += itemWidth;
426
+ }
427
+ return styledCharsToString(lineWithinWidth).trimEnd();
322
428
  })
323
429
  .join('\n');
324
430
  return {
@@ -1,4 +1,7 @@
1
- import widestLine from 'widest-line';
1
+ // [mixdog fork] use the shared display-width policy so the wrap decision here
2
+ // agrees with measure-text.js and output.js. Kept in sync with
3
+ // src/tui/display-width.mjs.
4
+ import { displayWidestLine as widestLine } from './display-width.js';
2
5
  import indentString from 'indent-string';
3
6
  import Yoga from 'yoga-layout';
4
7
  import wrapText from './wrap-text.js';
@@ -41,6 +41,10 @@ const render = (node, options) => {
41
41
  getSelectionText: () => instance.selectedText,
42
42
  // [mixdog fork] resolve the word rect at a cell for double-click select.
43
43
  getWordRectAt: (x, y) => instance.getWordRectAt(x, y),
44
+ // [mixdog fork] resolve the whole-line rect at a row for triple-click /
45
+ // line select. Symmetric to getWordRectAt; index.jsx wires it into the
46
+ // store as store.getLineRectAt.
47
+ getLineRectAt: (y) => instance.getLineRectAt(y),
44
48
  };
45
49
  };
46
50
  export default render;
@@ -1,170 +0,0 @@
1
- 'use strict';
2
- /**
3
- * permission-rules.cjs
4
- * Pattern matcher and priority evaluator. Priority order and Tool(content)
5
- * parsing mirror Claude Code native logic; specifier matching uses a local
6
- * path/command glob (native delegates per-tool content matching to each tool).
7
- *
8
- * Priority (highest → lowest): deny > ask > allow > mode default
9
- *
10
- * Pattern forms:
11
- * mcp__* → toolName starts with "mcp__"
12
- * ToolName → exact toolName match (no parens)
13
- * ToolName(*) → toolName matches AND specifier glob matches any
14
- * ToolName(specifier) → toolName matches AND specifier glob matches
15
- * toolInput.path / .command / .file
16
- * Glob chars: * (any chars within segment), ** (any segments)
17
- *
18
- * No external deps; no eval/Function.
19
- */
20
-
21
- // Read-only tool set (mixdog tool names).
22
- const READ_ONLY_TOOLS = new Set([
23
- 'read', 'list', 'glob', 'grep', 'find_symbol', 'find_references', 'find_callers', 'explore',
24
- 'recall', 'search', 'fetch', 'web_fetch',
25
- 'schedule_status', 'list_models', 'job_wait', 'download_attachment',
26
- ]);
27
-
28
- /**
29
- * Find the index of the first/last UNescaped occurrence of a char.
30
- * A char is escaped when preceded by an odd number of backslashes.
31
- * Mirrors native permissionRuleParser.findFirst/LastUnescapedChar.
32
- */
33
- function findFirstUnescapedChar(str, char) {
34
- for (let i = 0; i < str.length; i++) {
35
- if (str[i] !== char) continue;
36
- let bs = 0, j = i - 1;
37
- while (j >= 0 && str[j] === '\\') { bs++; j--; }
38
- if (bs % 2 === 0) return i;
39
- }
40
- return -1;
41
- }
42
-
43
- function findLastUnescapedChar(str, char) {
44
- for (let i = str.length - 1; i >= 0; i--) {
45
- if (str[i] !== char) continue;
46
- let bs = 0, j = i - 1;
47
- while (j >= 0 && str[j] === '\\') { bs++; j--; }
48
- if (bs % 2 === 0) return i;
49
- }
50
- return -1;
51
- }
52
-
53
- /** Reverse escapeRuleContent: \( -> (, \) -> ), \\ -> \ (order matters). */
54
- function unescapeRuleContent(content) {
55
- return content
56
- .replace(/\\\(/g, '(')
57
- .replace(/\\\)/g, ')')
58
- .replace(/\\\\/g, '\\');
59
- }
60
-
61
- /**
62
- * Convert a glob pattern string to a RegExp.
63
- * Supports: * (non-slash wildcard), ** (any), ? (single char).
64
- * Safe: no eval.
65
- */
66
- function globToRegex(glob) {
67
- let src = '';
68
- let i = 0;
69
- while (i < glob.length) {
70
- const ch = glob[i];
71
- if (ch === '*' && glob[i + 1] === '*') {
72
- src += '.*';
73
- i += 2;
74
- if (glob[i] === '/') i++; // consume trailing slash
75
- } else if (ch === '*') {
76
- src += '[^/\\\\]*';
77
- i++;
78
- } else if (ch === '?') {
79
- src += '[^/\\\\]';
80
- i++;
81
- } else {
82
- // Escape regex metacharacters
83
- src += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
84
- i++;
85
- }
86
- }
87
- return new RegExp('^' + src + '$', 'i');
88
- }
89
-
90
- /**
91
- * Match a single pattern entry against (toolName, toolInput).
92
- * Returns true if the pattern applies.
93
- */
94
- function matchesPattern(pattern, toolName, toolInput) {
95
- if (typeof pattern !== 'string') return false;
96
-
97
- // Detect Tool(specifier) form. Escape-aware, mirroring native
98
- // permissionRuleValueFromString: only UNescaped parens delimit the
99
- // specifier, and the close paren must be the final char.
100
- const parenIdx = findFirstUnescapedChar(pattern, '(');
101
- const closeIdx = findLastUnescapedChar(pattern, ')');
102
- if (parenIdx > 0 && closeIdx === pattern.length - 1 && closeIdx > parenIdx) {
103
- const namePart = pattern.slice(0, parenIdx);
104
- const rawSpecifier = pattern.slice(parenIdx + 1, closeIdx);
105
-
106
- if (toolName !== namePart) return false;
107
-
108
- // Empty "()" or wildcard "(*)" specifier → tool-wide rule, match any
109
- if (rawSpecifier === '' || rawSpecifier === '*') return true;
110
-
111
- // Unescape \( \) before glob matching
112
- const specifier = unescapeRuleContent(rawSpecifier);
113
- // Match specifier against known path-bearing fields
114
- const specRe = globToRegex(specifier);
115
- const inp = toolInput || {};
116
- const candidates = [...new Set([
117
- inp.path, inp.command, inp.file, inp.file_path, inp.base_path, inp.cwd,
118
- ...(Array.isArray(inp.path) ? inp.path.map(p => (p && typeof p === 'object' ? p.path : p)) : []),
119
- ...(Array.isArray(inp.reads) ? inp.reads.map(r => r?.path) : []),
120
- ...(Array.isArray(inp.edits) ? inp.edits.flatMap(e => [e?.path, e?.file_path]) : []),
121
- ...(Array.isArray(inp.writes) ? inp.writes.flatMap(w => [w?.path, w?.file_path]) : []),
122
- ].filter(v => typeof v === 'string'))];
123
-
124
- return candidates.some(c => specRe.test(c));
125
- }
126
-
127
- // Prefix wildcard: e.g. "mcp__*"
128
- if (pattern.endsWith('*')) {
129
- const prefix = pattern.slice(0, -1);
130
- return toolName.startsWith(prefix);
131
- }
132
-
133
- // Glob on toolName itself (may contain * or **)
134
- if (pattern.includes('*') || pattern.includes('?')) {
135
- return globToRegex(pattern).test(toolName);
136
- }
137
-
138
- // Exact match
139
- return toolName === pattern;
140
- }
141
-
142
- /**
143
- * Evaluate priority rules against a tool call.
144
- *
145
- * @param {string} toolName
146
- * @param {object} toolInput
147
- * @param {string[]} allowList
148
- * @param {string[]} denyList
149
- * @param {string[]} askList
150
- * @returns {'deny'|'ask'|'allow'|null} null = no list matched (use mode default)
151
- */
152
- function evaluateRules(toolName, toolInput, allowList, denyList, askList) {
153
- if (denyList.some(p => matchesPattern(p, toolName, toolInput))) return 'deny';
154
- if (askList.some(p => matchesPattern(p, toolName, toolInput))) return 'ask';
155
- if (allowList.some(p => matchesPattern(p, toolName, toolInput))) return 'allow';
156
- return null;
157
- }
158
-
159
- /**
160
- * Determine whether a tool is considered read-only.
161
- * For mcp__ tools: strip the mcp__ prefix chain and check the short tool name.
162
- */
163
- function isReadOnlyTool(toolName) {
164
- // Strip mcp__plugin_...__ prefix (one or more double-underscore segments)
165
- const parts = toolName.split('__');
166
- const shortName = parts[parts.length - 1];
167
- return READ_ONLY_TOOLS.has(shortName);
168
- }
169
-
170
- module.exports = { matchesPattern, evaluateRules, isReadOnlyTool, READ_ONLY_TOOLS };
@@ -1,112 +0,0 @@
1
- 'use strict';
2
- /**
3
- * settings-loader.cjs
4
- * Loads and merges Mixdog settings from three tiers (lowest → highest):
5
- * 1. User global: $MIXDOG_CONFIG_DIR/settings.json (or ~/.mixdog/settings.json)
6
- * 2. Project: $MIXDOG_PROJECT_DIR/.mixdog/settings.json (or cwd/.mixdog/settings.json)
7
- * 3. Project local: $MIXDOG_PROJECT_DIR/.mixdog/settings.local.json
8
- *
9
- * Only `permissions` sub-tree is returned; the rest is ignored.
10
- * Pure fs/path — no external deps.
11
- */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const os = require('os');
16
-
17
- // ── In-process mtime-driven cache ────────────────────────────────────────────
18
- // Keyed by resolved file path. Each entry: { mtime: number|null, data: any }.
19
- // Invalidated per-tier independently when mtime changes.
20
- // TTL: never-expire by default (mtime-driven only).
21
- const _fileCache = new Map();
22
-
23
- function readJsonCached(filePath) {
24
- let entry = _fileCache.get(filePath);
25
- let mtime = null;
26
- try { mtime = fs.statSync(filePath).mtimeMs; } catch { /* file absent */ }
27
- if (entry && entry.mtime === mtime) return entry.data;
28
- let data = null;
29
- if (mtime !== null) {
30
- try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { data = null; }
31
- }
32
- _fileCache.set(filePath, { mtime, data });
33
- return data;
34
- }
35
-
36
- /** Clear the entire settings cache (for test isolation). */
37
- function clearSettingsCache() {
38
- _fileCache.clear();
39
- }
40
-
41
- /**
42
- * Merge two permissions objects (base then overlay).
43
- * Arrays are concatenated and de-duped; scalar fields are overwritten.
44
- */
45
- function mergePermissions(base, overlay) {
46
- if (!overlay) return base || {};
47
- if (!base) return overlay || {};
48
-
49
- const merged = Object.assign({}, base);
50
-
51
- for (const key of ['allow', 'deny', 'ask']) {
52
- const b = Array.isArray(base[key]) ? base[key] : [];
53
- const o = Array.isArray(overlay[key]) ? overlay[key] : [];
54
- const combined = [...b, ...o];
55
- if (combined.length) merged[key] = [...new Set(combined)];
56
- }
57
-
58
- // scalar: overlay wins
59
- if (overlay.defaultMode !== undefined) merged.defaultMode = overlay.defaultMode;
60
-
61
- return merged;
62
- }
63
-
64
- /**
65
- * Load and merge settings from all three tiers.
66
- * Returns `{ allow: string[], deny: string[], ask: string[], defaultMode: string }`.
67
- */
68
- function loadPermissions(projectDir) {
69
- const userConfigDir = process.env.MIXDOG_CONFIG_DIR ||
70
- path.join(os.homedir(), '.mixdog');
71
- const projDir = projectDir ||
72
- process.env.MIXDOG_PROJECT_DIR ||
73
- process.cwd();
74
-
75
- const userSettings = readJsonCached(path.join(userConfigDir, 'settings.json'));
76
- const projectSettings = readJsonCached(path.join(projDir, '.mixdog', 'settings.json'));
77
- const localSettings = readJsonCached(path.join(projDir, '.mixdog', 'settings.local.json'));
78
-
79
- // Accept permissions from `settings.permissions` (canonical) OR top-level
80
- // `allow`/`deny`/`ask`/`defaultMode` fields (common user shorthand).
81
- function extractPerms(s) {
82
- if (!s) return {};
83
- const nested = (s.permissions && typeof s.permissions === 'object') ? s.permissions : {};
84
- const topLevel = {};
85
- for (const key of ['allow', 'deny', 'ask']) {
86
- if (Array.isArray(s[key])) topLevel[key] = s[key];
87
- }
88
- if (typeof s.defaultMode === 'string') topLevel.defaultMode = s.defaultMode;
89
- return mergePermissions(nested, topLevel);
90
- }
91
- const userPerms = extractPerms(userSettings);
92
- const projectPerms = extractPerms(projectSettings);
93
- const localPerms = extractPerms(localSettings);
94
-
95
- let merged = mergePermissions({}, userPerms);
96
- merged = mergePermissions(merged, projectPerms);
97
- merged = mergePermissions(merged, localPerms);
98
-
99
- const VALID_MODES = new Set(['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions', 'auto']);
100
- const rawMode = merged.defaultMode;
101
- const resolvedMode = (typeof rawMode === 'string' && VALID_MODES.has(rawMode))
102
- ? rawMode
103
- : 'default';
104
- return {
105
- allow: Array.isArray(merged.allow) ? merged.allow : [],
106
- deny: Array.isArray(merged.deny) ? merged.deny : [],
107
- ask: Array.isArray(merged.ask) ? merged.ask : [],
108
- defaultMode: resolvedMode,
109
- };
110
- }
111
-
112
- module.exports = { loadPermissions, clearSettingsCache };
@@ -1,10 +0,0 @@
1
- 'use strict';
2
-
3
- // Single source for the hook IPC path. Centralizes an existing platform
4
- // contract (not new heuristic). Rust mirror native/mixdog-shim/src/main.rs:34-43
5
- // must stay in sync.
6
- module.exports = function hookPipePath() {
7
- return process.platform === 'win32'
8
- ? '\\\\.\\pipe\\mixdog-hooks'
9
- : require('path').join(process.env.XDG_RUNTIME_DIR || '/tmp', 'mixdog-hooks.sock');
10
- };
@@ -1,20 +0,0 @@
1
- ---
2
- name: extreme-simple
3
- title: Extreme Simple
4
- description: Ultra-short completion sentence
5
- aliases: extreme, extreme simple
6
- keep-coding-instructions: true
7
- ---
8
-
9
- # Output Style
10
-
11
- Extreme — one ultra-short completion sentence. The most minimal of the styles.
12
-
13
- - Reply with a single completion sentence whenever possible.
14
- - Preferred pattern: `<target> 변경되었습니다. <verification> 통과 완료입니다.`
15
- - If verification was not run, use one short sentence saying the change is done
16
- and verification was not run.
17
- - No headings, bullets, labels, explanations, tool traces, or metadata unless
18
- the user explicitly asks.
19
- - Preserve only the single decisive path, command, symbol, API name, code, or
20
- error verbatim.
@@ -1,51 +0,0 @@
1
- # Workflow
2
-
3
- Priority:
4
- - When an `# Active Workflow` block is present, it is the binding session
5
- workflow and takes precedence over all other workflow guidance.
6
-
7
- Delegation:
8
- - Lead is the supervisor of agents, responsible for task delegation,
9
- coordination, judgment, and final decisions.
10
- - As supervisor, prefer delegating implementation work to an agent; keep direct
11
- work limited to coordination and User Workflow exceptions such as
12
- one-or-two-step edits, pre-planning, config changes, and final git deployment.
13
- - Implementation edits to product/runtime/TUI code MUST be delegated to a
14
- Worker/Heavy Worker, even when small. Lead may directly edit only rules, docs,
15
- config, and final deployment/git steps, unless the user explicitly asks Lead
16
- to patch directly.
17
- - Delegation split: different code paths / entry points / file groups are
18
- separate scopes — implementation AND analysis, review, debugging split the
19
- same way. With 2+ independent scopes, spawn each scoped agent in the
20
- SAME turn, not one after another. Shared functions or cross-cutting concerns
21
- do NOT justify collapsing them into one delegation: split per path, and Lead
22
- verifies the cross-cutting parts (shared state, telemetry consistency, etc.)
23
- directly. "One agent must see the whole context" is not a valid reason to
24
- merge; the only legitimate single-scope case is a genuinely inseparable
25
- dependency — and then state the reason.
26
- - Follow-up work on the same path or scope MUST reuse the existing agent (same
27
- tag). Spawn a new agent only for a genuinely separate scope.
28
- - Pick the agent role defined in the User Workflow section that fits the task
29
- (worker, heavy-worker, reviewer, debugger, …) when spawning.
30
- - Write agent-facing briefs and follow-up task messages in English.
31
- - Agent briefs must be bounded: state the goal/success criteria, known starting
32
- files or anchors if available, allowed/forbidden changes, expected
33
- deliverable, and verification boundary. For heavy-worker, include known first
34
- components to inspect and the stopping condition; complex work must not
35
- become open-ended.
36
-
37
- Agent lifecycle:
38
- - After spawning async agent(s), END THE TURN immediately. Do not poll
39
- status/read, guess the outcome, or start dependent work — treat the result as
40
- unknown until the completion notification resumes you. status/read are manual
41
- recovery only, never progress checks.
42
- - Unless instructed otherwise, wait for the spawned agent(s); once their results
43
- are collected, automatically continue with the next turn.
44
-
45
- Agent result handling:
46
- - Cross-check material agent responses before using them for decisions or
47
- summarizing them to the user.
48
- - When an agent returns, always synthesize its result into a concise report for
49
- the user (outcome + key changes/evidence). Never forward the raw agent output.
50
- - If anything here conflicts with the User Workflow section, this Workflow takes
51
- precedence, except that the `# Active Workflow` block always wins.