mixdog 0.9.43 → 0.9.44

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 (32) hide show
  1. package/package.json +3 -2
  2. package/scripts/ansi-color-capability-test.mjs +90 -0
  3. package/scripts/generate-runtime-manifest.mjs +39 -22
  4. package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
  5. package/scripts/internal-comms-smoke.mjs +24 -7
  6. package/scripts/maintenance-default-routes-test.mjs +164 -0
  7. package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
  8. package/scripts/routing-corpus.mjs +1 -1
  9. package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
  10. package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
  11. package/scripts/tui-transcript-jitter-harness.mjs +58 -0
  12. package/src/agents/reviewer/AGENT.md +5 -7
  13. package/src/rules/lead/lead-brief.md +5 -4
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
  15. package/src/runtime/agent/orchestrator/config.mjs +29 -14
  16. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
  17. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
  19. package/src/runtime/memory/data/runtime-manifest.json +11 -11
  20. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
  21. package/src/standalone/agent-tool.mjs +65 -50
  22. package/src/standalone/explore-tool.mjs +17 -16
  23. package/src/tui/app/provider-setup-picker.mjs +1 -0
  24. package/src/tui/app/use-transcript-window.mjs +5 -1
  25. package/src/tui/components/StatusLine.jsx +7 -6
  26. package/src/tui/dist/index.mjs +213 -77
  27. package/src/tui/engine/turn.mjs +1 -1
  28. package/src/tui/index.jsx +3 -2
  29. package/src/tui/markdown/streaming-markdown.mjs +35 -9
  30. package/src/tui/statusline-ansi-bridge.mjs +17 -7
  31. package/src/ui/ansi.mjs +85 -5
  32. package/src/ui/statusline-format.mjs +7 -7
package/src/tui/index.jsx CHANGED
@@ -15,6 +15,7 @@ import { App } from './App.jsx';
15
15
  import { createEngineSession } from './engine.mjs';
16
16
  import { scheduleRenderFrameAck } from './engine/render-timing.mjs';
17
17
  import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
18
+ import { rgbSgr } from '../ui/ansi.mjs';
18
19
  import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
19
20
  import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
20
21
  import { displayWidth } from './display-width.mjs';
@@ -259,10 +260,10 @@ function resolveTuiStderrLogPath() {
259
260
  || join(process.env.MIXDOG_RUNTIME_ROOT || join(tmpdir(), 'mixdog'), 'mixdog-tui.stderr.log');
260
261
  }
261
262
 
262
- /** `rgb(r,g,b)` → truecolor SGR prefix ('' when unparsable). */
263
+ /** `rgb(r,g,b)` → supported RGB SGR prefix ('' when unparsable). */
263
264
  function ansiFg(rgb) {
264
265
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ''));
265
- return m ? `\x1b[38;2;${m[1]};${m[2]};${m[3]}m` : '';
266
+ return m ? rgbSgr(m[1], m[2], m[3]) : '';
266
267
  }
267
268
 
268
269
  /**
@@ -7,6 +7,8 @@ import { configureMarked, hasMarkdownSyntax } from './render-ansi.mjs';
7
7
  import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
8
8
 
9
9
  const stablePrefixByStreamKey = new Map();
10
+ // Reuse the current normalized-text split across measure → render → harvest.
11
+ const resolvedPartsByStreamKey = new Map();
10
12
  const STABLE_PREFIX_LRU_MAX = 32;
11
13
 
12
14
  /** Lockstep with App streaming row estimate (leading/trailing newline trim). */
@@ -37,6 +39,25 @@ function getStablePrefixKey(key) {
37
39
  return value;
38
40
  }
39
41
 
42
+ function getResolvedPartsKey(key, text) {
43
+ if (!key) return null;
44
+ const entry = resolvedPartsByStreamKey.get(key);
45
+ if (!entry || entry.text !== text) return null;
46
+ return entry.parts;
47
+ }
48
+
49
+ function cacheResolvedPartsKey(key, text, parts) {
50
+ if (!key) return parts;
51
+ if (resolvedPartsByStreamKey.has(key)) resolvedPartsByStreamKey.delete(key);
52
+ resolvedPartsByStreamKey.set(key, { text, parts });
53
+ while (resolvedPartsByStreamKey.size > STABLE_PREFIX_LRU_MAX) {
54
+ const oldest = resolvedPartsByStreamKey.keys().next().value;
55
+ if (oldest === undefined) break;
56
+ resolvedPartsByStreamKey.delete(oldest);
57
+ }
58
+ return parts;
59
+ }
60
+
40
61
  function hasOpenFence(text) {
41
62
  let ticks = 0;
42
63
  let tildes = 0;
@@ -93,35 +114,40 @@ export function balanceStreamingMarkdown(text) {
93
114
 
94
115
  export function resetStreamingMarkdownStablePrefix(streamKey) {
95
116
  if (streamKey == null || streamKey === '') return;
96
- stablePrefixByStreamKey.delete(String(streamKey));
117
+ const key = String(streamKey);
118
+ stablePrefixByStreamKey.delete(key);
119
+ resolvedPartsByStreamKey.delete(key);
97
120
  }
98
121
 
99
122
  export function resetAllStreamingMarkdownStablePrefixes() {
100
123
  stablePrefixByStreamKey.clear();
124
+ resolvedPartsByStreamKey.clear();
101
125
  }
102
126
 
103
127
  export function resolveStreamingMarkdownParts(text, streamKey) {
104
128
  const t = streamingLayoutText(text);
105
129
  const key = streamKey == null || streamKey === '' ? null : String(streamKey);
130
+ const cachedParts = getResolvedPartsKey(key, t);
131
+ if (cachedParts) return cachedParts;
106
132
 
107
133
  if (!t) {
108
134
  if (key) stablePrefixByStreamKey.delete(key);
109
- return {
135
+ return cacheResolvedPartsKey(key, t, {
110
136
  plain: true,
111
137
  stablePrefix: '',
112
138
  unstableSuffix: '',
113
139
  unstableForRender: '',
114
- };
140
+ });
115
141
  }
116
142
 
117
143
  if (!hasMarkdownSyntax(t)) {
118
144
  if (key) stablePrefixByStreamKey.delete(key);
119
- return {
145
+ return cacheResolvedPartsKey(key, t, {
120
146
  plain: true,
121
147
  stablePrefix: '',
122
148
  unstableSuffix: t,
123
149
  unstableForRender: t,
124
- };
150
+ });
125
151
  }
126
152
 
127
153
  let stablePrefix = key ? getStablePrefixKey(key) : '';
@@ -141,13 +167,13 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
141
167
  if (key && openPrefix) touchStablePrefixKey(key, openPrefix);
142
168
  else if (key) stablePrefixByStreamKey.delete(key);
143
169
  const unstableSuffix = t.substring(openPrefix.length);
144
- return {
170
+ return cacheResolvedPartsKey(key, t, {
145
171
  plain: false,
146
172
  openFence: true,
147
173
  stablePrefix: openPrefix,
148
174
  unstableSuffix,
149
175
  unstableForRender: unstableSuffix,
150
- };
176
+ });
151
177
  }
152
178
 
153
179
  try {
@@ -179,10 +205,10 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
179
205
  if (isWhitespaceOnlyText(stablePrefix)) stablePrefix = '';
180
206
 
181
207
  const unstableSuffix = t.substring(stablePrefix.length);
182
- return {
208
+ return cacheResolvedPartsKey(key, t, {
183
209
  plain: false,
184
210
  stablePrefix,
185
211
  unstableSuffix,
186
212
  unstableForRender: balanceStreamingMarkdown(unstableSuffix),
187
- };
213
+ });
188
214
  }
@@ -6,6 +6,8 @@
6
6
  * normalizes that at display time so `/theme` can re-tone without touching ui/.
7
7
  */
8
8
 
9
+ import { rgbToAnsi256 } from '../ui/ansi.mjs';
10
+
9
11
  const RESET = '\x1b[0m';
10
12
 
11
13
  /** Truecolor SGR sequences emitted by the shared statusline stack (not theme). */
@@ -23,6 +25,14 @@ function truecolorSgr(r, g, b) {
23
25
  return `\x1b[38;2;${r};${g};${b}m`;
24
26
  }
25
27
 
28
+ function ansi256Sgr(r, g, b) {
29
+ return `\x1b[38;5;${rgbToAnsi256(r, g, b)}m`;
30
+ }
31
+
32
+ function canonicalSgrVariants(rgb) {
33
+ return [truecolorSgr(...rgb), ansi256Sgr(...rgb)];
34
+ }
35
+
26
36
  function footerLocalNum(value) {
27
37
  const n = Number(value);
28
38
  return Number.isFinite(n) ? n : 0;
@@ -139,13 +149,13 @@ export function applyDefaultStatusForegroundAfterReset(text, STATUS, reset = RES
139
149
  export function remapCanonicalStatuslineTruecolor(text, colors) {
140
150
  const c = STATUSLINE_CANONICAL_TRUECOLOR;
141
151
  const pairs = [
142
- [truecolorSgr(...c.statusText), colors.STATUS],
143
- [truecolorSgr(...c.subtle), colors.SUBTLE],
144
- [truecolorSgr(...c.success), colors.SUCCESS],
145
- [truecolorSgr(...c.successBright), colors.SUCCESS],
146
- [truecolorSgr(...c.warning), colors.WARNING],
147
- [truecolorSgr(...c.warningBright), colors.WARNING],
148
- [truecolorSgr(...c.error), colors.ERROR],
152
+ ...canonicalSgrVariants(c.statusText).map((from) => [from, colors.STATUS]),
153
+ ...canonicalSgrVariants(c.subtle).map((from) => [from, colors.SUBTLE]),
154
+ ...canonicalSgrVariants(c.success).map((from) => [from, colors.SUCCESS]),
155
+ ...canonicalSgrVariants(c.successBright).map((from) => [from, colors.SUCCESS]),
156
+ ...canonicalSgrVariants(c.warning).map((from) => [from, colors.WARNING]),
157
+ ...canonicalSgrVariants(c.warningBright).map((from) => [from, colors.WARNING]),
158
+ ...canonicalSgrVariants(c.error).map((from) => [from, colors.ERROR]),
149
159
  ];
150
160
  let out = String(text || '');
151
161
  for (const [from, to] of pairs) {
package/src/ui/ansi.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
  * The decision is computed once at import time but can be recomputed via
13
13
  * `refreshColorSupport()` (used by tests / when output is rebound).
14
14
  */
15
- import { stdout, env } from 'node:process';
15
+ import { stdout, env, platform } from 'node:process';
16
16
 
17
17
  let COLOR_ENABLED = computeColorEnabled();
18
18
 
@@ -39,12 +39,92 @@ export function colorEnabled() {
39
39
  const ESC = '\x1b[';
40
40
  const RESET = `${ESC}0m`;
41
41
 
42
+ /**
43
+ * Whether the terminal can render 24-bit SGR colors.
44
+ *
45
+ * Apple Terminal is the one explicit negative because it advertises a normal
46
+ * xterm TERM while not implementing truecolor. Unknown terminals retain the
47
+ * historical truecolor default.
48
+ */
49
+ export function supportsTruecolor(environment = env, platformName = platform) {
50
+ const termProgram = String(environment?.TERM_PROGRAM || '').trim().toLowerCase();
51
+ if (termProgram === 'apple_terminal') return false;
52
+
53
+ const colorTerm = String(environment?.COLORTERM || '').trim().toLowerCase();
54
+ if (colorTerm === 'truecolor' || colorTerm === '24bit') return true;
55
+ if (environment?.WT_SESSION !== undefined && environment.WT_SESSION !== '') return true;
56
+ if (['iterm.app', 'wezterm', 'ghostty', 'vscode'].includes(termProgram)) return true;
57
+ if (/(?:direct|truecolor)/i.test(String(environment?.TERM || ''))) return true;
58
+ if (platformName === 'win32') return true;
59
+ return true;
60
+ }
61
+
62
+ const ANSI_256_CUBE_LEVELS = Object.freeze([0, 95, 135, 175, 215, 255]);
63
+
64
+ function colorByte(value) {
65
+ const n = Number(value);
66
+ return Number.isFinite(n) ? Math.max(0, Math.min(255, Math.round(n))) : 0;
67
+ }
68
+
69
+ function nearestCubeLevel(value) {
70
+ let best = 0;
71
+ let bestDistance = Infinity;
72
+ for (let i = 0; i < ANSI_256_CUBE_LEVELS.length; i++) {
73
+ const distance = Math.abs(value - ANSI_256_CUBE_LEVELS[i]);
74
+ if (distance < bestDistance) {
75
+ best = i;
76
+ bestDistance = distance;
77
+ }
78
+ }
79
+ return best;
80
+ }
81
+
82
+ /** Convert an RGB triplet to the nearest stable xterm 256-color palette index. */
83
+ export function rgbToAnsi256(r, g, b) {
84
+ const red = colorByte(r);
85
+ const green = colorByte(g);
86
+ const blue = colorByte(b);
87
+
88
+ const redLevel = nearestCubeLevel(red);
89
+ const greenLevel = nearestCubeLevel(green);
90
+ const blueLevel = nearestCubeLevel(blue);
91
+ const cubeRed = ANSI_256_CUBE_LEVELS[redLevel];
92
+ const cubeGreen = ANSI_256_CUBE_LEVELS[greenLevel];
93
+ const cubeBlue = ANSI_256_CUBE_LEVELS[blueLevel];
94
+ const cubeDistance = ((red - cubeRed) ** 2)
95
+ + ((green - cubeGreen) ** 2)
96
+ + ((blue - cubeBlue) ** 2);
97
+
98
+ const average = (red + green + blue) / 3;
99
+ const grayLevel = Math.max(0, Math.min(23, Math.round((average - 8) / 10)));
100
+ const grayValue = 8 + (grayLevel * 10);
101
+ const grayDistance = ((red - grayValue) ** 2)
102
+ + ((green - grayValue) ** 2)
103
+ + ((blue - grayValue) ** 2);
104
+
105
+ if (grayDistance < cubeDistance) return 232 + grayLevel;
106
+ return 16 + (36 * redLevel) + (6 * greenLevel) + blueLevel;
107
+ }
108
+
109
+ /** Raw RGB SGR prefix, downsampled to 256 colors when truecolor is unavailable. */
110
+ export function rgbSgr(r, g, b, background = false) {
111
+ const channel = background ? 48 : 38;
112
+ if (supportsTruecolor()) return `${ESC}${channel};2;${r};${g};${b}m`;
113
+ return `${ESC}${channel};5;${rgbToAnsi256(r, g, b)}m`;
114
+ }
115
+
116
+ function supportedSgrOpen(open) {
117
+ const match = /^(38|48);2;([^;]+);([^;]+);([^;]+)$/.exec(String(open));
118
+ if (!match || supportsTruecolor()) return open;
119
+ return `${match[1]};5;${rgbToAnsi256(match[2], match[3], match[4])}`;
120
+ }
121
+
42
122
  /** Build a `(text) => string` wrapper for the given SGR open code(s). */
43
123
  function sgr(open) {
44
- const openSeq = `${ESC}${open}m`;
45
124
  return (text) => {
46
125
  if (!COLOR_ENABLED) return String(text ?? '');
47
126
  const s = String(text ?? '');
127
+ const openSeq = `${ESC}${supportedSgrOpen(open)}m`;
48
128
  // Re-open after any nested reset so composed styles survive concatenation.
49
129
  return openSeq + s.replace(/\x1b\[0m/g, RESET + openSeq) + RESET;
50
130
  };
@@ -84,9 +164,9 @@ export const bgGray = sgr('100');
84
164
  export const bgBlack = sgr('40');
85
165
  export const bgBlue = sgr('44');
86
166
 
87
- // --- Truecolor (24-bit) -----------------------------------------------------
88
- // The TUI palette uses explicit rgb() values. 16-color SGR can't reproduce them, so we emit
89
- // 24-bit SGR. Honors NO_COLOR / TTY exactly like the named helpers above.
167
+ // --- RGB colors --------------------------------------------------------------
168
+ // Emit 24-bit SGR where supported, otherwise use the nearest 256-color entry.
169
+ // Honors NO_COLOR / TTY exactly like the named helpers above.
90
170
 
91
171
  /** Foreground truecolor wrapper: `rgb(215,119,87)('x')`. */
92
172
  export function rgb(r, g, b) {
@@ -5,7 +5,7 @@
5
5
  * bar/segment formatters, small numeric helpers, and the TUI-independent
6
6
  * formatElapsed() replica. No behavior change — statusline.mjs re-imports these.
7
7
  */
8
- import { colorEnabled, rgb } from './ansi.mjs';
8
+ import { colorEnabled, rgb, rgbSgr } from './ansi.mjs';
9
9
  import { displayModelName, shortenModelName } from './model-display.mjs';
10
10
  import { getModelMetadataSync } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
11
11
 
@@ -23,12 +23,12 @@ function sgr(code) {
23
23
 
24
24
  export const R = sgr('0');
25
25
  export const B = sgr('1');
26
- export const D = sgr('38;2;136;136;136');
27
- export const GRN = sgr('38;2;0;170;75');
28
- export const YLW = sgr('38;2;255;193;7');
29
- export const RED = sgr('38;2;220;70;88');
30
- export const CYN = sgr('38;2;136;136;136');
31
- export const GREY = sgr('38;2;136;136;136');
26
+ export const D = colorEnabled() ? rgbSgr(136, 136, 136) : '';
27
+ export const GRN = colorEnabled() ? rgbSgr(0, 170, 75) : '';
28
+ export const YLW = colorEnabled() ? rgbSgr(255, 193, 7) : '';
29
+ export const RED = colorEnabled() ? rgbSgr(220, 70, 88) : '';
30
+ export const CYN = colorEnabled() ? rgbSgr(136, 136, 136) : '';
31
+ export const GREY = colorEnabled() ? rgbSgr(136, 136, 136) : '';
32
32
 
33
33
  export function terminalColumns() {
34
34
  const cols = Number(process.stdout?.columns);