mixdog 0.9.26 → 0.9.28
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/package.json +7 -2
- package/scripts/arg-guard-test.mjs +68 -0
- package/scripts/compacted-placeholder-scrub-test.mjs +77 -0
- package/src/app.mjs +14 -0
- package/src/cli.mjs +40 -4
- package/src/rules/shared/01-tool.md +3 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/staged-install-worker.mjs +14 -0
- package/src/runtime/shared/staged-update.mjs +530 -0
- package/src/runtime/shared/update-checker.mjs +1 -1
- package/src/session-runtime/lifecycle-api.mjs +9 -11
- package/src/session-runtime/runtime-core.mjs +61 -40
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/tui/App.jsx +2 -1
- package/src/tui/app/transcript-window.mjs +26 -6
- package/src/tui/components/Spinner.jsx +5 -2
- package/src/tui/components/StatusLine.jsx +9 -9
- package/src/tui/components/ToolExecution.jsx +34 -59
- package/src/tui/display-width.mjs +8 -4
- package/src/tui/dist/index.mjs +545 -425
- package/src/tui/engine/notification-plan.mjs +5 -1
- package/src/tui/hooks/useSharedTick.mjs +103 -0
- package/src/tui/index.jsx +2 -1
- package/src/tui/markdown/format-token.mjs +46 -8
- package/src/tui/markdown/render-ansi.mjs +24 -1
- package/src/tui/markdown/stream-fence.mjs +60 -0
- package/src/tui/markdown/streaming-markdown.mjs +22 -1
- package/src/workflows/default/WORKFLOW.md +12 -8
- package/vendor/ink/build/display-width.js +5 -4
|
@@ -64,7 +64,11 @@ export function resolveTuiRuntimeNotificationDelivery(event, text) {
|
|
|
64
64
|
// UI-only notices (e.g. boot auto-update outcome): render as a transcript
|
|
65
65
|
// notice, never enqueue anything model-visible.
|
|
66
66
|
if (meta.kind === 'update-notice') {
|
|
67
|
-
|
|
67
|
+
// Wording lives here (the notice surface), not in the emitting runtime:
|
|
68
|
+
// the emitter only supplies meta.version; the sentence is composed here.
|
|
69
|
+
const ver = String(meta.version || '').trim();
|
|
70
|
+
const displayText = ver ? `mixdog v${ver} ready — restart to apply.` : trimmed;
|
|
71
|
+
return { action: 'notice', displayText, tone: meta.tone === 'warn' ? 'warn' : 'info' };
|
|
68
72
|
}
|
|
69
73
|
if (!isExecutionNotification(event, trimmed, parsed)) {
|
|
70
74
|
return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks/useSharedTick.mjs — one process-wide animation timer for the TUI.
|
|
3
|
+
*
|
|
4
|
+
* The transcript used to fan out timers: Spinner's useAnimation, a 500ms blink
|
|
5
|
+
* + 1s elapsed setInterval PER pending ToolExecution card, and StatusLine's own
|
|
6
|
+
* refresh interval. With N pending cards that is 1 + 2N + 1 OS timers all waking
|
|
7
|
+
* the event loop independently, which showed up in the stutter bench as +369
|
|
8
|
+
* CPU-ms/s and 143ms frame gaps while idle-animated.
|
|
9
|
+
*
|
|
10
|
+
* This collapses every animated component onto a SINGLE shared setInterval.
|
|
11
|
+
* Subscribers register the cadence they want; the shared timer runs at the
|
|
12
|
+
* finest requested interval and only notifies each subscriber once its own
|
|
13
|
+
* interval has elapsed (derived from Date.now()), so a 500ms blink and a 130ms
|
|
14
|
+
* spinner still animate at their own rates off one timer. When the last
|
|
15
|
+
* subscriber unmounts the timer is cleared, so nothing ticks when nothing
|
|
16
|
+
* animated is on screen. Components derive blink phase / elapsed seconds /
|
|
17
|
+
* spinner frame from Date.now() at render — the tick only forces the re-render.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useRef, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
// Boundary tolerance: fire a subscriber a hair early rather than skip a whole
|
|
22
|
+
// base-interval and drift its cadence by a full frame.
|
|
23
|
+
const TICK_SLOP_MS = 16;
|
|
24
|
+
|
|
25
|
+
const subscribers = new Set();
|
|
26
|
+
let timer = null;
|
|
27
|
+
|
|
28
|
+
// Delay until the soonest subscriber is due (clamped to >= 1ms). A single timer
|
|
29
|
+
// is always aimed at the next real boundary, so a subscriber's cadence is tied
|
|
30
|
+
// to ITS OWN lastFire + interval — never to a shared base that resets when the
|
|
31
|
+
// finest-interval subscriber leaves. Returns null when nothing is subscribed.
|
|
32
|
+
function nextDelayMs(now) {
|
|
33
|
+
let soonest = Infinity;
|
|
34
|
+
for (const sub of subscribers) {
|
|
35
|
+
const due = sub.lastFire + sub.interval - now;
|
|
36
|
+
if (due < soonest) soonest = due;
|
|
37
|
+
}
|
|
38
|
+
return Number.isFinite(soonest) ? Math.max(1, soonest) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Aim the single timer at the next due boundary. Called on every subscribe /
|
|
42
|
+
// unsubscribe and after each tick, so adding a faster subscriber pulls the next
|
|
43
|
+
// wake earlier and removing the finest one re-aims at the remaining subscribers'
|
|
44
|
+
// own due times (no full fresh-interval stall). Clears the timer when empty.
|
|
45
|
+
function reconcileTimer() {
|
|
46
|
+
if (timer) {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
timer = null;
|
|
49
|
+
}
|
|
50
|
+
const delay = nextDelayMs(Date.now());
|
|
51
|
+
if (delay == null) return;
|
|
52
|
+
timer = setTimeout(onTick, delay);
|
|
53
|
+
timer.unref?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onTick() {
|
|
57
|
+
timer = null;
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
// Snapshot so a subscriber unsubscribing during notify can't mutate the set
|
|
60
|
+
// mid-iteration.
|
|
61
|
+
for (const sub of Array.from(subscribers)) {
|
|
62
|
+
if (!subscribers.has(sub)) continue;
|
|
63
|
+
if (now - sub.lastFire >= sub.interval - TICK_SLOP_MS) {
|
|
64
|
+
sub.lastFire = now;
|
|
65
|
+
sub.notify(now);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
reconcileTimer();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe the calling component to the shared tick.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} intervalMs desired re-render cadence in ms.
|
|
75
|
+
* @param {boolean} [isActive=true] unsubscribe (and let the timer stop) when false.
|
|
76
|
+
* @param {(now:number)=>void} [onTick] optional callback fired on each tick
|
|
77
|
+
* instead of the default internal re-render (e.g. to bump an existing state).
|
|
78
|
+
*/
|
|
79
|
+
export function useSharedTick(intervalMs, isActive = true, onTick = null) {
|
|
80
|
+
const [, setTick] = useState(0);
|
|
81
|
+
const cbRef = useRef(onTick);
|
|
82
|
+
cbRef.current = onTick;
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!isActive) return undefined;
|
|
85
|
+
const interval = Math.max(1, Number(intervalMs) || 0);
|
|
86
|
+
const sub = {
|
|
87
|
+
interval,
|
|
88
|
+
lastFire: Date.now(),
|
|
89
|
+
notify: (now) => {
|
|
90
|
+
if (cbRef.current) cbRef.current(now);
|
|
91
|
+
else setTick((t) => (t + 1) % 1_000_000);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
subscribers.add(sub);
|
|
95
|
+
reconcileTimer();
|
|
96
|
+
return () => {
|
|
97
|
+
subscribers.delete(sub);
|
|
98
|
+
reconcileTimer();
|
|
99
|
+
};
|
|
100
|
+
}, [intervalMs, isActive]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default useSharedTick;
|
package/src/tui/index.jsx
CHANGED
|
@@ -18,6 +18,7 @@ import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './the
|
|
|
18
18
|
import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
19
19
|
import { displayWidth } from './display-width.mjs';
|
|
20
20
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
21
|
+
import { localPackageVersion } from '../runtime/shared/update-checker.mjs';
|
|
21
22
|
|
|
22
23
|
// Trailing `\x1b[>0s` restores XTSHIFTESCAPE (shift-to-select-extend) to its
|
|
23
24
|
// terminal default; MOUSE_TRACKING_ON opts into `\x1b[>1s`, so every mouse/alt
|
|
@@ -290,7 +291,7 @@ function paintBootSplash() {
|
|
|
290
291
|
out += `${bold}${fg}${center(logo[i])}${reset}\r\n`;
|
|
291
292
|
}
|
|
292
293
|
out += '\r\n';
|
|
293
|
-
out += `${subtleFg}${center(`mixdog coding agent · ${process.cwd()}`)}${reset}`;
|
|
294
|
+
out += `${subtleFg}${center(`mixdog coding agent · v${localPackageVersion()} · ${process.cwd()}`)}${reset}`;
|
|
294
295
|
|
|
295
296
|
// Park the cursor at home so ink's first frame paints top-down over the
|
|
296
297
|
// splash instead of starting at the bottom row and scrolling the screen.
|
|
@@ -550,6 +550,36 @@ function collectFlatBodyLines(text, codeBlock, bandWidth) {
|
|
|
550
550
|
return lines;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
// Streaming open-fence (`token.plain`) fast normalize: strip raw TAB / C0
|
|
554
|
+
// controls (so no stray byte survives to bypass the viewport clip) WITHOUT the
|
|
555
|
+
// per-character displayWidth loop of normalizeCodeText — column-accurate tab
|
|
556
|
+
// stops are cosmetic and are restored when the block settles and re-renders.
|
|
557
|
+
function normalizeCodePlain(text) {
|
|
558
|
+
return String(text ?? '')
|
|
559
|
+
.replace(/\r\n?/g, '\n')
|
|
560
|
+
.replace(/\t/g, ' ')
|
|
561
|
+
// C0 controls + DEL except LF → single space.
|
|
562
|
+
.replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, ' ');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Flat body for the streaming open block: color each line, wrapping only lines
|
|
566
|
+
// that actually overflow (the common code line fits, so no wrapAnsi call). The
|
|
567
|
+
// displayWidth guard is skipped for over-length lines (they always wrap) and for
|
|
568
|
+
// blanks, keeping the per-frame pass close to O(n) with a small constant.
|
|
569
|
+
function collectPlainBodyLines(text, codeBlock, bandWidth) {
|
|
570
|
+
const max = Math.max(1, bandWidth);
|
|
571
|
+
const lines = [];
|
|
572
|
+
for (const line of String(text ?? '').split(EOL)) {
|
|
573
|
+
if (!line) { lines.push(''); continue; }
|
|
574
|
+
if (line.length <= max && displayWidth(line) <= max) {
|
|
575
|
+
lines.push(codeBlock(line));
|
|
576
|
+
} else {
|
|
577
|
+
for (const seg of wrapCodeLine(codeBlock(line), max)) lines.push(seg);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return lines;
|
|
581
|
+
}
|
|
582
|
+
|
|
553
583
|
function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
|
|
554
584
|
const highlighted = highlightCodeText(text, hljsLang);
|
|
555
585
|
const { codeBlock } = colorizers();
|
|
@@ -576,22 +606,30 @@ function renderCodeBlock(token, width = 0) {
|
|
|
576
606
|
const { codeBlock } = colorizers();
|
|
577
607
|
const c = extraColorizers();
|
|
578
608
|
const lang = normalizeLang(token.lang);
|
|
579
|
-
const text = normalizeCodeText(decodeEntities(token.text ?? ''));
|
|
580
609
|
const renderWidth = Math.max(8, Number(width) || 80);
|
|
581
610
|
// Wrap content to the render width minus the left gutter so `gutter + content`
|
|
582
611
|
// never overruns the available width.
|
|
583
612
|
const contentWidth = Math.max(1, renderWidth - CODE_GUTTER.length);
|
|
584
613
|
|
|
585
614
|
let bodyLines;
|
|
586
|
-
if (
|
|
587
|
-
|
|
615
|
+
if (token.plain) {
|
|
616
|
+
// Streaming open fence: flat, un-highlighted body via the cheap normalize +
|
|
617
|
+
// conditional wrap. Avoids marked's lexer AND highlight.js AND the per-char
|
|
618
|
+
// width normalize every delta; the settled block re-renders fully below.
|
|
619
|
+
const text = normalizeCodePlain(decodeEntities(token.text ?? ''));
|
|
620
|
+
bodyLines = collectPlainBodyLines(text, codeBlock, contentWidth);
|
|
588
621
|
} else {
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
bodyLines = collectHighlightedBodyLines(text, hljsLang, contentWidth);
|
|
622
|
+
const text = normalizeCodeText(decodeEntities(token.text ?? ''));
|
|
623
|
+
if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
|
|
624
|
+
bodyLines = collectDiffBodyLines(text, c, contentWidth);
|
|
593
625
|
} else {
|
|
594
|
-
|
|
626
|
+
const hljsLang = resolveHljsLanguage(lang);
|
|
627
|
+
const family = LANG_FAMILY[lang];
|
|
628
|
+
if (hljsLang && family !== 'md') {
|
|
629
|
+
bodyLines = collectHighlightedBodyLines(text, hljsLang, contentWidth);
|
|
630
|
+
} else {
|
|
631
|
+
bodyLines = collectFlatBodyLines(text, codeBlock, contentWidth);
|
|
632
|
+
}
|
|
595
633
|
}
|
|
596
634
|
}
|
|
597
635
|
// Indent every body row by the gutter (no bg band).
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { marked } from 'marked';
|
|
17
17
|
import { formatToken } from './format-token.mjs';
|
|
18
|
-
import { trimPartialClosingFences } from './stream-fence.mjs';
|
|
18
|
+
import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
|
|
19
19
|
|
|
20
20
|
const TOKEN_CACHE_MAX = 500;
|
|
21
21
|
const tokenCache = new Map();
|
|
@@ -50,6 +50,29 @@ export function lexMarkdown(content, { trimPartialFences = false } = {}) {
|
|
|
50
50
|
// are transient per-delta and trimming mutates them in place, so they are NOT
|
|
51
51
|
// cached — caching would corrupt the shared cache for stable text.
|
|
52
52
|
if (trimPartialFences) {
|
|
53
|
+
// Open-fence fast path: an unclosed fenced code block would make marked
|
|
54
|
+
// re-scan the whole growing block every delta (its closing-fence regex
|
|
55
|
+
// never matches and backtracks over the entire body — ~O(n²) per frame).
|
|
56
|
+
// Build the `code` token directly from the raw fence text instead, and mark
|
|
57
|
+
// it `plain` so it renders flat (no per-frame highlight.js pass) until the
|
|
58
|
+
// closing fence arrives; the settled (closed) frame lexes normally.
|
|
59
|
+
const open = findOpenFenceStart(text);
|
|
60
|
+
if (open) {
|
|
61
|
+
const post = text.slice(open.index);
|
|
62
|
+
const nl = post.indexOf('\n');
|
|
63
|
+
const codeToken = {
|
|
64
|
+
type: 'code',
|
|
65
|
+
raw: post,
|
|
66
|
+
lang: open.lang,
|
|
67
|
+
text: nl === -1 ? '' : post.slice(nl + 1),
|
|
68
|
+
plain: true,
|
|
69
|
+
};
|
|
70
|
+
const pre = text.slice(0, open.index);
|
|
71
|
+
const tokens = pre ? marked.lexer(pre) : [];
|
|
72
|
+
tokens.push(codeToken);
|
|
73
|
+
trimPartialClosingFences(tokens);
|
|
74
|
+
return tokens;
|
|
75
|
+
}
|
|
53
76
|
const tokens = marked.lexer(text);
|
|
54
77
|
trimPartialClosingFences(tokens);
|
|
55
78
|
return tokens;
|
|
@@ -32,3 +32,63 @@ export function trimPartialClosingFences(tokens) {
|
|
|
32
32
|
if (lastLine.length >= marker.length || lastLine !== marker[0].repeat(lastLine.length)) return;
|
|
33
33
|
token.text = String(token.text ?? '').slice(0, -lastLine.length).replace(/\n$/, '');
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Locate the currently-open (unclosed) fenced code block that is safe to split
|
|
38
|
+
* off cheaply, or null. Returns { index, lang } where `index` is the byte offset
|
|
39
|
+
* of the opening fence line's first char. Pure line scan (no marked.lexer), so a
|
|
40
|
+
* growing open code block avoids marked's catastrophic never-matched-closer
|
|
41
|
+
* backtracking every delta.
|
|
42
|
+
*
|
|
43
|
+
* Only a TOP-LEVEL fence (opening line at column 0) is reported: a col-0 fence
|
|
44
|
+
* is always a fresh top-level block in CommonMark (list/blockquote content is
|
|
45
|
+
* never at column 0), so everything before it is complete blocks and the split
|
|
46
|
+
* is render-invariant. An indented (possibly list/blockquote-nested) open fence
|
|
47
|
+
* returns null so the caller falls back to the correct full marked.lexer path.
|
|
48
|
+
*
|
|
49
|
+
* Fence rules are marked/CommonMark-aligned:
|
|
50
|
+
* - opening: 0–3 spaces indent, run of ≥3 same fence char; a backtick fence's
|
|
51
|
+
* info string may not contain a backtick;
|
|
52
|
+
* - closing: 0–3 spaces indent, a run of fence chars that STARTS with the exact
|
|
53
|
+
* opening marker (same char, ≥ opening length) and may continue with any mix
|
|
54
|
+
* of backticks/tildes (marked's `\1[~`]*`), followed only by spaces/tabs.
|
|
55
|
+
*/
|
|
56
|
+
const OPEN_FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
57
|
+
const CLOSE_FENCE_RE = /^ {0,3}([`~]+)[ \t]*$/;
|
|
58
|
+
|
|
59
|
+
function isClosingFence(line, char, openLen) {
|
|
60
|
+
const m = CLOSE_FENCE_RE.exec(line);
|
|
61
|
+
if (!m) return false;
|
|
62
|
+
const run = m[1];
|
|
63
|
+
// marked closes on ` {0,3}\1[~`]* *`: the run must begin with the exact opening
|
|
64
|
+
// marker (openLen × openChar); trailing chars may be the OPPOSITE fence char
|
|
65
|
+
// (e.g. ``` closed by ```~~), so only the prefix is constrained.
|
|
66
|
+
return run.startsWith(char.repeat(openLen));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function findOpenFenceStart(text) {
|
|
70
|
+
const value = String(text ?? '');
|
|
71
|
+
let open = null;
|
|
72
|
+
let start = 0;
|
|
73
|
+
for (let i = 0; i <= value.length; i++) {
|
|
74
|
+
if (i !== value.length && value[i] !== '\n') continue;
|
|
75
|
+
const line = value.slice(start, i);
|
|
76
|
+
if (!open) {
|
|
77
|
+
const m = OPEN_FENCE_RE.exec(line);
|
|
78
|
+
if (m) {
|
|
79
|
+
const char = m[2][0];
|
|
80
|
+
const info = m[3];
|
|
81
|
+
// A backtick fence's info string may not contain a backtick.
|
|
82
|
+
if (!(char === '`' && info.indexOf('`') !== -1)) {
|
|
83
|
+
open = { index: start, indent: m[1].length, char, len: m[2].length, lang: info.trim() };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else if (isClosingFence(line, open.char, open.len)) {
|
|
87
|
+
open = null;
|
|
88
|
+
}
|
|
89
|
+
start = i + 1;
|
|
90
|
+
}
|
|
91
|
+
// Only fast-path unambiguously top-level (column-0) fences.
|
|
92
|
+
if (!open || open.indent !== 0) return null;
|
|
93
|
+
return { index: open.index, lang: open.lang };
|
|
94
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { marked } from 'marked';
|
|
6
6
|
import { configureMarked, hasMarkdownSyntax } from './render-ansi.mjs';
|
|
7
|
-
import { trimPartialClosingFences } from './stream-fence.mjs';
|
|
7
|
+
import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
|
|
8
8
|
|
|
9
9
|
const stablePrefixByStreamKey = new Map();
|
|
10
10
|
const STABLE_PREFIX_LRU_MAX = 32;
|
|
@@ -129,6 +129,27 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
|
|
|
129
129
|
stablePrefix = '';
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Open-fence fast path: never run marked.lexer on a growing unclosed code
|
|
133
|
+
// block (its closing-fence regex never matches and backtracks over the whole
|
|
134
|
+
// body every delta — the ~56ms/frame cost). Split cheaply at the fence line:
|
|
135
|
+
// everything before it is settled markdown (lexed + cached once by the stable
|
|
136
|
+
// <Markdown>), the open block is rendered flat until the closing fence lands.
|
|
137
|
+
const open = findOpenFenceStart(t);
|
|
138
|
+
if (open) {
|
|
139
|
+
let openPrefix = t.substring(0, open.index);
|
|
140
|
+
if (isWhitespaceOnlyText(openPrefix)) openPrefix = '';
|
|
141
|
+
if (key && openPrefix) touchStablePrefixKey(key, openPrefix);
|
|
142
|
+
else if (key) stablePrefixByStreamKey.delete(key);
|
|
143
|
+
const unstableSuffix = t.substring(openPrefix.length);
|
|
144
|
+
return {
|
|
145
|
+
plain: false,
|
|
146
|
+
openFence: true,
|
|
147
|
+
stablePrefix: openPrefix,
|
|
148
|
+
unstableSuffix,
|
|
149
|
+
unstableForRender: unstableSuffix,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
132
153
|
try {
|
|
133
154
|
configureMarked();
|
|
134
155
|
const boundary = stablePrefix.length;
|
|
@@ -13,19 +13,23 @@ exploration only. Diagnosis agreement or problem-pointing is NOT approval.
|
|
|
13
13
|
|
|
14
14
|
Lead supervises: delegates, coordinates, judges, decides. Route by complexity
|
|
15
15
|
(after approval):
|
|
16
|
-
- Lead directly:
|
|
17
|
-
|
|
16
|
+
- Lead directly: only one-step fixes doable in a single turn, plus
|
|
17
|
+
coordination, config, and git deployment. Everything else delegates.
|
|
18
|
+
- Worker: any multi-step or multi-file implementation. Heavy Worker:
|
|
19
|
+
high-complexity scopes.
|
|
18
20
|
- Reviewer: verify implementation scopes. Debugger: very high complexity, or
|
|
19
21
|
root-causing already failed once.
|
|
20
22
|
|
|
21
23
|
1. Plan — present a draft plan before ANY implementation; if not approved,
|
|
22
24
|
revise and re-present (ping-pong) until an explicit go-ahead.
|
|
23
|
-
2. Delegate — split
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
2. Delegate — maximize distribution: split the work into as many independent
|
|
26
|
+
scopes as possible and hand each to its own agent, all spawned in the SAME
|
|
27
|
+
turn (parallel by default; sequential steps only inside one complex scope,
|
|
28
|
+
gated build/test-green). This applies to every role alike — worker,
|
|
29
|
+
heavy-worker, reviewer, debugger: fan them out across agents, never one at
|
|
30
|
+
a time. Only a genuinely inseparable single scope stays whole, and say so.
|
|
31
|
+
Briefs per the Lead brief contract. After spawning async agents, END THE
|
|
32
|
+
TURN.
|
|
29
33
|
3. Review — pair one reviewer 1:1 per implementation scope, same turn.
|
|
30
34
|
Cross-check agent results yourself; send fixes back to the original scope
|
|
31
35
|
and loop fix -> re-verify until clean. Skip only for simple low-risk work.
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* overlap gets worse, not better.
|
|
11
11
|
*
|
|
12
12
|
* Policy: widen ONLY U+2460–U+24FF (enclosed alphanumerics / circled digits)
|
|
13
|
-
* and U+
|
|
14
|
-
* (U+2500–U+257F), block elements,
|
|
13
|
+
* and U+2194–U+21FF (arrows) to 2 cells when ON. NEVER widen box-drawing
|
|
14
|
+
* (U+2500–U+257F), block elements, the four Basic Arrows U+2190–U+2193
|
|
15
|
+
* (← ↑ → ↓ — figures.mjs 1-cell markers), or other ambiguous glyphs. ON by default
|
|
15
16
|
* on Windows (win32) or under Windows Terminal (WT_SESSION);
|
|
16
17
|
* MIXDOG_TUI_AMBIGUOUS_WIDE='1'/'0' overrides and wins. OFF ⇒ identical to
|
|
17
18
|
* plain string-width.
|
|
@@ -23,14 +24,14 @@ import stringWidth from 'string-width';
|
|
|
23
24
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
24
25
|
|
|
25
26
|
function isProblemCodePoint(cp) {
|
|
26
|
-
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >=
|
|
27
|
+
return (cp >= 0x2460 && cp <= 0x24ff) || (cp >= 0x2194 && cp <= 0x21ff);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// [mixdog fork] Fast precheck for the problem ranges above. Lets the hot path
|
|
30
31
|
// bail before the per-grapheme segmenter loop when a string (the common
|
|
31
32
|
// ASCII/status-text case) contains no widenable glyph. Kept in sync with
|
|
32
33
|
// src/tui/display-width.mjs.
|
|
33
|
-
const PROBLEM_RE = /[\
|
|
34
|
+
const PROBLEM_RE = /[\u2194-\u21ff\u2460-\u24ff]/;
|
|
34
35
|
|
|
35
36
|
function resolveAmbiguousWidePolicy(env = process.env, platform = process.platform) {
|
|
36
37
|
const override = env?.MIXDOG_TUI_AMBIGUOUS_WIDE;
|