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.
- package/README.md +1 -4
- package/package.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/channel-daemon-smoke.mjs +165 -0
- package/scripts/channel-daemon-stub.mjs +69 -0
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/tool-smoke.mjs +30 -17
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +187 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +24 -11
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +126 -167
- package/src/runtime/channels/lib/owner-heartbeat.mjs +11 -60
- package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
- package/src/runtime/channels/lib/seat-lock.mjs +196 -0
- package/src/runtime/channels/lib/session-discovery.mjs +2 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +24 -8
- package/src/runtime/channels/lib/worker-main.mjs +42 -11
- package/src/runtime/memory/index.mjs +54 -7
- package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
- package/src/runtime/memory/lib/pg/process.mjs +85 -40
- package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
- package/src/runtime/shared/atomic-file.mjs +20 -4
- package/src/runtime/shared/tool-primitives.mjs +31 -1
- package/src/runtime/shared/tool-surface.mjs +23 -13
- package/src/session-runtime/config-lifecycle.mjs +48 -7
- package/src/session-runtime/lifecycle-api.mjs +9 -0
- package/src/session-runtime/mcp-glue.mjs +63 -1
- package/src/session-runtime/resource-api.mjs +62 -8
- package/src/session-runtime/runtime-core.mjs +32 -2
- package/src/session-runtime/session-text.mjs +41 -0
- package/src/session-runtime/session-turn-api.mjs +24 -0
- package/src/session-runtime/settings-api.mjs +8 -1
- package/src/session-runtime/tool-catalog.mjs +306 -38
- package/src/session-runtime/tool-defs.mjs +7 -7
- package/src/session-runtime/workflow.mjs +2 -1
- package/src/standalone/channel-daemon-client.mjs +224 -0
- package/src/standalone/channel-daemon-transport.mjs +351 -0
- package/src/standalone/channel-daemon.mjs +139 -0
- package/src/standalone/channel-worker.mjs +213 -4
- package/src/standalone/hook-bus.mjs +71 -3
- package/src/tui/App.jsx +105 -17
- package/src/tui/app/clipboard.mjs +39 -19
- package/src/tui/app/doctor.mjs +57 -0
- package/src/tui/app/extension-pickers.mjs +53 -9
- package/src/tui/app/maintenance-pickers.mjs +0 -5
- package/src/tui/app/slash-dispatch.mjs +4 -4
- package/src/tui/app/text-layout.mjs +11 -0
- package/src/tui/app/use-mouse-input.mjs +235 -51
- package/src/tui/app/use-prompt-handlers.mjs +49 -30
- package/src/tui/app/use-transcript-scroll.mjs +124 -27
- package/src/tui/app/use-transcript-window.mjs +55 -1
- package/src/tui/components/Message.jsx +1 -1
- package/src/tui/components/PromptInput.jsx +3 -1
- package/src/tui/components/QueuedCommands.jsx +21 -10
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +635 -306
- package/src/tui/engine/session-api.mjs +17 -7
- package/src/tui/engine/session-flow.mjs +6 -0
- package/src/tui/engine.mjs +8 -0
- package/src/tui/index.jsx +62 -18
- package/src/tui/paste-attachments.mjs +26 -0
- package/src/ui/statusline.mjs +45 -5
- package/src/ui/tool-card.mjs +8 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
- package/src/workflows/bench/WORKFLOW.md +46 -0
- package/src/workflows/default/WORKFLOW.md +6 -0
- package/src/workflows/solo/WORKFLOW.md +5 -0
- package/vendor/ink/build/ink.js +23 -1
- package/vendor/ink/build/output.js +154 -71
- package/vendor/ink/build/render-node-to-output.js +44 -2
- package/vendor/ink/build/render.js +4 -0
- 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
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
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
|
-
//
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
438
|
+
...(cell ?? { value: ' ' }),
|
|
439
|
+
value,
|
|
440
|
+
styles: [...effective, selectionBg],
|
|
370
441
|
};
|
|
371
442
|
}
|
|
372
|
-
// Trailing spaces
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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` : '',
|