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
@@ -97,7 +97,7 @@ export default class Output {
97
97
  this.selection = rect;
98
98
  }
99
99
  write(x, y, text, options) {
100
- const { transformers } = options;
100
+ const { transformers, softWrap } = options;
101
101
  if (!text) {
102
102
  return;
103
103
  }
@@ -107,6 +107,10 @@ export default class Output {
107
107
  y,
108
108
  text,
109
109
  transformers,
110
+ // [mixdog fork] per-visual-line soft-wrap flags, parallel to
111
+ // text.split('\n'); softWrap[i]=true means line i is a word-wrap
112
+ // continuation of line i-1. undefined = producer didn't track it.
113
+ softWrap,
110
114
  });
111
115
  }
112
116
  clip(clip) {
@@ -135,6 +139,11 @@ export default class Output {
135
139
  }
136
140
  output.push(row);
137
141
  }
142
+ // [mixdog fork] per-output-row soft-wrap continuation bitmap, filled from
143
+ // each write op's softWrap flags at the row it lands on. rowSoftWrap[y]
144
+ // true = row y is a word-wrap continuation of y-1. Left false where the
145
+ // producer didn't track wrapping (fills, raw ansi, unwrapped text).
146
+ const rowSoftWrap = new Array(this.height).fill(false);
138
147
  const clips = [];
139
148
  for (const operation of this.operations) {
140
149
  if (operation.type === 'clip') {
@@ -144,9 +153,12 @@ export default class Output {
144
153
  clips.pop();
145
154
  }
146
155
  if (operation.type === 'write') {
147
- const { text, transformers } = operation;
156
+ const { text, transformers, softWrap } = operation;
148
157
  let { x, y } = operation;
149
158
  let lines = text.split('\n');
159
+ // Index of the first surviving line after a vertical clip slice,
160
+ // so softWrap[] (parallel to the UN-clipped lines) stays aligned.
161
+ let swFrom = 0;
150
162
  // [mixdog fork] Nested overflow:hidden boxes must be clipped to
151
163
  // the INTERSECTION of every active ancestor clip. Using only the
152
164
  // innermost clip lets child components such as ToolExecution /
@@ -190,6 +202,7 @@ export default class Output {
190
202
  const height = lines.length;
191
203
  const to = y + height > clip.y2 ? clip.y2 - y : height;
192
204
  lines = lines.slice(from, to);
205
+ swFrom = from;
193
206
  if (y < clip.y1) {
194
207
  y = clip.y1;
195
208
  }
@@ -212,6 +225,13 @@ export default class Output {
212
225
  offsetY++;
213
226
  continue;
214
227
  }
228
+ // [mixdog fork] record this VISIBLE row's soft-wrap origin.
229
+ // Fail closed: a write WITHOUT softWrap metadata clears any
230
+ // prior sw bit on the row, so untracked overlapping content
231
+ // (fills, raw ansi, a later plain write) can never inherit a
232
+ // stale continuation flag and misjoin. Placed after the
233
+ // empty-line guard so clipped-away no-op lines don't clear it.
234
+ rowSoftWrap[y + offsetY] = softWrap ? softWrap[swFrom + index] === true : false;
215
235
  const spaceCell = {
216
236
  type: 'char',
217
237
  value: ' ',
@@ -290,12 +310,53 @@ export default class Output {
290
310
  // every colored glyph to one color. selectionForeground is now
291
311
  // intentionally unused (kept in the setter API for compatibility).
292
312
  const selectionBg = rgbStyle(sel.selectionBackground, 'background', [245, 245, 245]);
293
- // A style entry sets the background when its opening SGR code begins
294
- // with a background parameter: 40-47 (basic), 48 (256/truecolor),
295
- // 49 (default), or 100-107 (bright). ansi-tokenize emits one entry
296
- // per SGR attribute, so a prefix test on `.code` cleanly isolates the
297
- // bg-only entries we want to drop before appending the selection bg.
298
- const isBackgroundStyle = (s) => typeof s?.code === 'string' && /^\x1b\[(?:4[0-9]|10[0-7])[;m]/.test(s.code);
313
+ // Parse each SGR entry's parameter list and remove ONLY the bg and
314
+ // inverse params, keeping everything else (fg, bold/dim/underline,
315
+ // etc.) in order, then re-emit the sequence. A prefix test is not
316
+ // enough: a compound entry like \x1b[31;48;5;42m or
317
+ // \x1b[1;48;2;r;g;b;37m would keep its old bg and override the
318
+ // selection color. Background params: 40-49 (basic + default),
319
+ // 100-107 (bright), 48;5;n (256), 48;2;r;g;b (truecolor). Inverse: 7.
320
+ // Returns a sanitized style, or null when no params remain (drop).
321
+ const sanitizeStyle = (s) => {
322
+ if (typeof s?.code !== 'string')
323
+ return s ?? null;
324
+ const m = /^\x1b\[([\d;]*)m$/.exec(s.code);
325
+ if (!m)
326
+ return s;
327
+ const params = m[1].length ? m[1].split(';') : [];
328
+ const kept = [];
329
+ for (let i = 0; i < params.length; i++) {
330
+ const n = Number(params[i]);
331
+ if (n === 7) // inverse
332
+ continue;
333
+ // basic/default/bright bg — 48 is EXCLUDED here: it is the
334
+ // extended-bg introducer (48;5;n / 48;2;r;g;b) and must be
335
+ // consumed WITH its payload below. Matching it in this range
336
+ // check dropped only the lone '48' and left '2;r;g;b' behind,
337
+ // which re-parsed as dim(2)+basic colors and grayed out any
338
+ // text drawn over a truecolor background (user-message band).
339
+ if ((n >= 40 && n <= 47) || n === 49 || (n >= 100 && n <= 107))
340
+ continue;
341
+ if (n === 48) { // extended bg: 48;5;n or 48;2;r;g;b
342
+ const mode = Number(params[i + 1]);
343
+ i += mode === 2 ? 4 : mode === 5 ? 2 : 1;
344
+ continue;
345
+ }
346
+ if (n === 38) { // extended fg — keep the whole group
347
+ const mode = Number(params[i + 1]);
348
+ const span = mode === 2 ? 4 : mode === 5 ? 2 : 1;
349
+ for (let j = 0; j <= span && i + j < params.length; j++)
350
+ kept.push(params[i + j]);
351
+ i += span;
352
+ continue;
353
+ }
354
+ kept.push(params[i]);
355
+ }
356
+ if (kept.length === 0)
357
+ return null;
358
+ return { ...s, code: `\x1b[${kept.join(';')}m` };
359
+ };
299
360
  const linear = sel.mode === 'linear';
300
361
  const start = linear && (sel.y1 > sel.y2 || (sel.y1 === sel.y2 && sel.x1 > sel.x2))
301
362
  ? { x: sel.x2, y: sel.y2 }
@@ -308,20 +369,21 @@ export default class Output {
308
369
  const y1 = Math.max(0, clipY1, linear ? start.y : Math.min(sel.y1, sel.y2));
309
370
  const y2 = Math.min(this.height - 1, clipY2, linear ? end.y : Math.max(sel.y1, sel.y2));
310
371
  const lineMode = linear && y1 !== y2;
311
- const selRows = captureSelectedText ? [] : null;
312
372
  // [mixdog fork] selRowObjs is built UNCONDITIONALLY whenever a
313
373
  // selection rect exists, independent of captureSelectedText: a
314
374
  // capture-disabled drag (rect.captureText === false) still needs a
315
375
  // per-row harvest so getSelectionRows() can stitch tall selections.
316
- // Only selectedText/selRows honor the captureSelectedText gate.
376
+ // Each entry also carries sw = rowSoftWrap[y] so the copy join (here
377
+ // and the app-side stitch) can rejoin word-wrap continuations. Only
378
+ // selectedText honors the captureSelectedText gate.
317
379
  const selRowObjs = [];
318
380
  for (let y = y1; y <= y2; y++) {
319
381
  const row = output[y];
382
+ // [mixdog fork] is THIS row a soft-wrap continuation of the row
383
+ // above? Drives the logical-line rejoin at copy time.
384
+ const sw = rowSoftWrap[y] === true;
320
385
  if (!row) {
321
- selRowObjs.push({ y, text: '' });
322
- if (captureSelectedText) {
323
- selRows.push('');
324
- }
386
+ selRowObjs.push({ y, text: '', sw });
325
387
  continue;
326
388
  }
327
389
  const rawX1 = linear
@@ -332,49 +394,65 @@ export default class Output {
332
394
  : Math.max(sel.x1, sel.x2);
333
395
  const x1 = Math.max(0, Math.min(rawX1, rawX2));
334
396
  const x2 = Math.min(row.length - 1, Math.max(rawX1, rawX2));
335
- let contentStart = -1;
336
- let contentEnd = -1;
337
- for (let x = x1; x <= x2; x++) {
338
- const value = row[x]?.value ?? '';
339
- if (value && !/^\s$/u.test(value)) {
340
- if (contentStart === -1) {
341
- contentStart = x;
342
- }
343
- contentEnd = x;
344
- }
345
- }
346
397
  let rowText = '';
398
+ // [mixdog fork] running active fg/attr SGR for carry-forward below;
399
+ // reset per selected row so state never bleeds across rows.
400
+ let carry = [];
347
401
  for (let x = x1; x <= x2; x++) {
348
402
  const cell = row[x];
349
- if (!cell) {
350
- continue;
351
- }
352
403
  // Collect the visible glyph. Wide-char trailing placeholders
353
404
  // carry value '' and contribute nothing, which is correct.
354
405
  // Always accumulated so the unconditional selRowObjs harvest
355
406
  // has row text even when captureSelectedText is false.
356
- rowText += cell.value ?? '';
357
- if (contentStart === -1 || x < contentStart || x > contentEnd) {
358
- continue;
407
+ rowText += cell?.value ?? '';
408
+ // [mixdog fork] Match claude-code: EVERY cell in the selected
409
+ // span gets ONE uniform selection background — no content-span
410
+ // skipping. Empty/whitespace cells (and gaps in the grid) are
411
+ // painted as a blank ' ' carrying only the selection bg, so a
412
+ // single highlight style covers text and padding alike.
413
+ // fg-preserving: keep the cell's own style entries (foreground
414
+ // color, bold/dim/underline, link attrs) and swap ONLY the
415
+ // background — drop any existing background/inverse entry so
416
+ // they don't fight the selection bg, then append it last.
417
+ const value = cell?.value ?? ' ';
418
+ const baseStyles = Array.isArray(cell?.styles) ? cell.styles : [];
419
+ const preserved = baseStyles.map(sanitizeStyle).filter(Boolean);
420
+ // [mixdog fork] Carry the row's active fg/attr SGR forward into
421
+ // selected cells that carry NO style of their own — grid-padding
422
+ // gaps and wide-char trailing placeholders sit at styles=[], so
423
+ // styledCharsToString's per-cell diff would emit a bare
424
+ // fg/attr RESET (\x1b[39m / 22m / 24m) at each such cell,
425
+ // BREAKING the colored run mid-selection and repainting the
426
+ // following glyphs from a reset state. Inherit the last active
427
+ // fg/attrs so the highlighted span stays ONE uniform SGR run.
428
+ // Guard: only blank/whitespace cells inherit — a VISIBLE glyph
429
+ // that genuinely has empty styles is intentional default-fg and
430
+ // must never be recolored.
431
+ if (preserved.length > 0) {
432
+ carry = preserved;
359
433
  }
360
- // [mixdog fork] fg-preserving highlight: keep the cell's own
361
- // style entries (foreground color, bold/dim/underline, link
362
- // attrs) and swap ONLY the background. Drop any existing
363
- // background entry so the two bgs don't fight, then append the
364
- // selection bg last so it wins for this cell.
365
- const baseStyles = Array.isArray(cell.styles) ? cell.styles : [];
366
- const preserved = baseStyles.filter(style => !isBackgroundStyle(style));
434
+ const effective = preserved.length > 0
435
+ ? preserved
436
+ : (value.trim() === '' ? carry : preserved);
367
437
  row[x] = {
368
- ...cell,
369
- styles: [...preserved, selectionBg],
438
+ ...(cell ?? { value: ' ' }),
439
+ value,
440
+ styles: [...effective, selectionBg],
370
441
  };
371
442
  }
372
- // Trailing spaces in a selected row are padding, not content.
373
- const trimmedRowText = rowText.replace(/\s+$/u, '');
374
- selRowObjs.push({ y, text: trimmedRowText });
375
- if (captureSelectedText) {
376
- selRows.push(trimmedRowText);
377
- }
443
+ // Trailing spaces are padding on a logical-line END, but at a
444
+ // soft-wrap boundary the break-point whitespace is the word
445
+ // separator (wrap-ansi keeps it on the head's tail or the
446
+ // continuation's lead). When the NEXT selected row continues this
447
+ // one, COLLAPSE the trailing run to a single space rather than
448
+ // trimming it: this preserves the one word-separator space while
449
+ // discarding the full-width padding blanks, so "alpha beta " +
450
+ // "gamma" rejoins as "alpha beta gamma" (not "alpha beta gamma").
451
+ // Hard/char-wrap rows have no trailing space, so nothing is added.
452
+ // Otherwise (logical-line end) trim trailing padding entirely.
453
+ const nextIsCont = y + 1 <= y2 && rowSoftWrap[y + 1] === true;
454
+ const rowOut = nextIsCont ? rowText.replace(/\s+$/u, ' ') : rowText.replace(/\s+$/u, '');
455
+ selRowObjs.push({ y, text: rowOut, sw });
378
456
  }
379
457
  // [mixdog fork] Outer-trim the per-row harvest (same rule as
380
458
  // selectedText below) but UNCONDITIONALLY, keeping the absolute row
@@ -387,31 +465,24 @@ export default class Output {
387
465
  }
388
466
  selectionRows = selRowObjs;
389
467
  if (captureSelectedText) {
390
- // Blank edge rows come from selecting through padded alt-screen
391
- // space around rendered content. Native terminal selection does
392
- // not paste those as leading/trailing empty lines, so trim only
393
- // the outer empty rows and preserve intentional blank rows inside.
394
- while (selRows.length > 0 && selRows[0].trim() === '') {
395
- selRows.shift();
396
- }
397
- while (selRows.length > 0 && selRows[selRows.length - 1].trim() === '') {
398
- selRows.pop();
399
- }
400
468
  // [mixdog fork] SOFT-WRAP JOIN (claude-code getSelectedText):
401
- // claude-code rejoins word-wrapped continuation rows into one
402
- // logical line using screen.softWrap, a per-row bitmap set at
403
- // wrap time marking a row as a continuation of the one above.
404
- // mixdog's rect(linear) render model carries NO softWrap metadata
405
- // the Output grid is a flat array of visual rows with no record
406
- // of which line breaks were inserted by wrapping vs. present in
407
- // the source. Detecting "this row filled to content width AND the
408
- // next continues the same logical block" from cell values alone is
409
- // a guess (a source line that legitimately fills the width would be
410
- // wrongly glued to the next). Per the correctness-over-cleverness
411
- // rule we keep the honest per-visual-row '\n' join rather than
412
- // fabricate wrap boundaries. Revisit if the render pipeline starts
413
- // emitting a softWrap bit per row.
414
- selectedText = selRows.join('\n');
469
+ // rejoin word-wrap continuation rows (sw = rowSoftWrap[y], set at
470
+ // render/output build time from the wrapText call) onto their
471
+ // logical source line WITHOUT a newline; only source/hard line
472
+ // breaks emit '\n'. selRowObjs is already outer-trimmed above so
473
+ // intentional interior blank rows (paragraph gaps) survive as
474
+ // empty logical lines. Trailing whitespace is trimmed once per
475
+ // logical-line end (mid-line separators kept by the harvest).
476
+ const logical = [];
477
+ for (const r of selRowObjs) {
478
+ if (r.sw && logical.length > 0) {
479
+ logical[logical.length - 1] += r.text;
480
+ }
481
+ else {
482
+ logical.push(r.text);
483
+ }
484
+ }
485
+ selectedText = logical.map(l => l.replace(/\s+$/u, '')).join('\n');
415
486
  }
416
487
  }
417
488
  // [mixdog fork] Snapshot per-row, column-indexed cell values so the App
@@ -449,7 +520,19 @@ export default class Output {
449
520
  lineWithinWidth.push(item);
450
521
  usedWidth += itemWidth;
451
522
  }
452
- return styledCharsToString(lineWithinWidth).trimEnd();
523
+ // [mixdog fork] Per-row SGR reset. styledCharsToString emits sticky
524
+ // SGR (color/dim/bold) but never a trailing reset, so a row that ends
525
+ // mid-attribute leaks its state into the NEXT row when log-update's
526
+ // incremental diff rewrites only changed rows (cursorTo(0)+row). That
527
+ // makes unrelated text visibly dim/darken during drag-selection
528
+ // repaints until a full repaint clears it. Terminate every row that
529
+ // carries any escape with a single explicit reset so each emitted row
530
+ // is self-contained. Reset ONCE at the row tail (not per cell) to
531
+ // avoid double-reset flicker; plain rows stay untouched.
532
+ const serialized = styledCharsToString(lineWithinWidth).trimEnd();
533
+ if (!serialized.includes('\x1b['))
534
+ return serialized;
535
+ return /\x1b\[0?m$/.test(serialized) ? serialized : serialized + '\x1b[0m';
453
536
  })
454
537
  .join('\n');
455
538
  return {
@@ -95,12 +95,54 @@ const renderNodeToOutput = (node, output, options) => {
95
95
  if (text.length > 0) {
96
96
  const currentWidth = widestLine(text);
97
97
  const maxWidth = getMaxWidth(yogaNode);
98
+ // [mixdog fork] soft-wrap continuation metadata, parallel to the
99
+ // FINAL text.split('\n'). softWrap[i]=true means visual line i is a
100
+ // word-wrap continuation of line i-1 (the '\n' before it was
101
+ // inserted by wrapText, not present in the source). This is the
102
+ // ONLY place both the pre-wrap source AND maxWidth are known, so
103
+ // the flags originate here and ride the write op to output.js,
104
+ // where the selection copy uses them to rejoin wrapped rows into
105
+ // their logical source line. Only 'wrap'/'hard' create
106
+ // continuations; truncate never does.
107
+ let softWrap;
98
108
  if (currentWidth > maxWidth) {
99
109
  const textWrap = node.style.textWrap ?? 'wrap';
110
+ const sourceLines = text.split('\n');
100
111
  text = wrapText(text, maxWidth, textWrap);
112
+ if (textWrap === 'wrap' || textWrap === 'hard') {
113
+ // Re-wrap each source logical line independently (wrap-ansi
114
+ // treats '\n' as a hard break, so per-line wrapping
115
+ // reproduces the whole-text row layout) and mark every
116
+ // produced line after the first as a continuation.
117
+ const flags = [];
118
+ for (const line of sourceLines) {
119
+ const parts = wrapText(line, maxWidth, textWrap).split('\n');
120
+ for (let i = 0; i < parts.length; i++) {
121
+ flags.push(i > 0);
122
+ }
123
+ }
124
+ // Guard: if per-line reflow disagrees with the whole-text
125
+ // row count, drop the metadata rather than emit a
126
+ // misaligned bitmap (output.js then keeps the honest '\n').
127
+ if (flags.length === text.split('\n').length) {
128
+ softWrap = flags;
129
+ }
130
+ }
101
131
  }
102
- text = applyPaddingToText(node, text);
103
- output.write(x, y, text, { transformers: newTransformers });
132
+ const padded = applyPaddingToText(node, text);
133
+ if (softWrap) {
134
+ // applyPaddingToText prepends offsetY blank lines (never
135
+ // continuations) and indents in place. Shift the flags by the
136
+ // number of prepended lines so they stay parallel to `padded`.
137
+ const added = padded.split('\n').length - text.split('\n').length;
138
+ if (added > 0) {
139
+ softWrap = new Array(added).fill(false).concat(softWrap);
140
+ }
141
+ if (softWrap.length !== padded.split('\n').length) {
142
+ softWrap = undefined;
143
+ }
144
+ }
145
+ output.write(x, y, padded, { transformers: newTransformers, softWrap });
104
146
  }
105
147
  return;
106
148
  }
@@ -50,6 +50,10 @@ const render = (node, options) => {
50
50
  // store.getRenderSelectionRows so the app can stitch selections taller
51
51
  // than the viewport.
52
52
  getSelectionRows: () => instance.getSelectionRows(),
53
+ // [mixdog fork] one-shot full clear+repaint of the next frame; the app
54
+ // uses it to dismiss Windows Terminal's persistent NATIVE (shift+drag)
55
+ // selection overlay before starting an app-owned selection.
56
+ forceFullRepaint: () => instance.forceFullRepaint(),
53
57
  };
54
58
  };
55
59
  export default render;
@@ -42,13 +42,16 @@ const renderer = (node, isScreenReaderEnabled, selection) => {
42
42
  skipStaticElements: false,
43
43
  });
44
44
  }
45
- const { output: generatedOutput, height: outputHeight, cursor, selectedText, plainRows } = output.get();
45
+ const { output: generatedOutput, height: outputHeight, cursor, selectedText, plainRows, selectionRows } = output.get();
46
46
  return {
47
47
  output: generatedOutput,
48
48
  outputHeight,
49
49
  cursor, // [mixdog fork] absolute cursor cell from the anchored input node, or null
50
50
  selectedText, // [mixdog fork] text inside the drag-selection, or null
51
51
  plainRows, // [mixdog fork] column-indexed cell values per row for word lookup
52
+ selectionRows, // [mixdog fork] per-row {y,text} selection harvest; ink.js caches
53
+ // it for getSelectionRows() so the app stitch harvest can reach rows
54
+ // that scrolled out of the viewport. Was dropped here → always null.
52
55
  // Newline at the end is needed, because static output doesn't have one, so
53
56
  // interactive output will override last line of static output
54
57
  staticOutput: staticOutput ? `${staticOutput.get().output}\n` : '',