mixdog 0.9.53 → 0.9.55

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 (77) hide show
  1. package/package.json +2 -1
  2. package/scripts/agent-model-liveness-test.mjs +68 -0
  3. package/scripts/anthropic-transport-policy-test.mjs +260 -0
  4. package/scripts/desktop-session-bridge-test.mjs +47 -0
  5. package/scripts/gemini-provider-test.mjs +396 -1
  6. package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
  7. package/scripts/interrupted-turn-history-test.mjs +28 -0
  8. package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
  9. package/scripts/pending-completion-drop-test.mjs +160 -4
  10. package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
  11. package/scripts/process-lifecycle-test.mjs +18 -4
  12. package/scripts/prompt-input-parity-test.mjs +145 -0
  13. package/scripts/provider-admission-scheduler-test.mjs +4 -5
  14. package/scripts/provider-contract-test.mjs +91 -33
  15. package/scripts/provider-toolcall-test.mjs +97 -17
  16. package/scripts/streaming-tail-window-test.mjs +146 -0
  17. package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
  18. package/scripts/tui-runtime-load-bench.mjs +45 -0
  19. package/scripts/tui-store-frame-batch-test.mjs +99 -0
  20. package/scripts/tui-transcript-perf-test.mjs +367 -2
  21. package/scripts/write-backpressure-test.mjs +147 -0
  22. package/src/cli.mjs +5 -5
  23. package/src/lib/keychain-cjs.cjs +114 -25
  24. package/src/repl.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
  26. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
  27. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
  28. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
  32. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
  41. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
  42. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
  43. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
  44. package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
  45. package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
  46. package/src/runtime/memory/lib/trace-store.mjs +66 -4
  47. package/src/runtime/shared/buffered-appender.mjs +83 -4
  48. package/src/runtime/shared/memory-snapshot.mjs +45 -36
  49. package/src/runtime/shared/process-lifecycle.mjs +166 -45
  50. package/src/runtime/shared/process-shutdown.mjs +2 -1
  51. package/src/session-runtime/model-route-api.mjs +4 -1
  52. package/src/session-runtime/native-search.mjs +4 -2
  53. package/src/session-runtime/provider-auth-api.mjs +8 -1
  54. package/src/session-runtime/provider-models.mjs +8 -3
  55. package/src/session-runtime/resource-api.mjs +2 -1
  56. package/src/session-runtime/runtime-core.mjs +32 -0
  57. package/src/session-runtime/session-turn-api.mjs +1 -0
  58. package/src/session-runtime/warmup-schedulers.mjs +5 -1
  59. package/src/standalone/agent-tool.mjs +19 -1
  60. package/src/tui/App.jsx +10 -4
  61. package/src/tui/app/text-layout.mjs +9 -1
  62. package/src/tui/app/transcript-window.mjs +146 -60
  63. package/src/tui/app/use-mouse-input.mjs +16 -8
  64. package/src/tui/app/use-transcript-scroll.mjs +71 -19
  65. package/src/tui/app/use-transcript-window.mjs +21 -10
  66. package/src/tui/components/PromptInput.jsx +13 -6
  67. package/src/tui/dist/index.mjs +1094 -232
  68. package/src/tui/engine/context-state.mjs +31 -31
  69. package/src/tui/engine/frame-batched-store.mjs +75 -0
  70. package/src/tui/engine/session-api-ext.mjs +6 -1
  71. package/src/tui/engine/session-api.mjs +9 -3
  72. package/src/tui/engine/session-flow.mjs +54 -27
  73. package/src/tui/engine/turn.mjs +44 -3
  74. package/src/tui/engine.mjs +515 -36
  75. package/src/tui/input-editing.mjs +33 -13
  76. package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
  77. package/vendor/ink/build/ink.js +8 -16
@@ -228,19 +228,39 @@ export function offsetAtCell(text, row, col, width) {
228
228
 
229
229
  function boundaryPositions(text, width) {
230
230
  const value = String(text || '');
231
- const offsets = [0];
232
- for (const unit of graphemeUnits(value)) offsets.push(unit.end);
233
-
234
- const seen = new Set();
235
- const positions = offsets
236
- .filter((offset) => {
237
- if (seen.has(offset)) return false;
238
- seen.add(offset);
239
- return true;
240
- })
241
- // Decide the flush-boundary advance from the FULL text so a soft-wrap
242
- // offset reports {row N+1, col 0} (matching ink) instead of {row N, col w}.
243
- .map((offset) => ({ offset, ...caretPosition(value, offset, width) }));
231
+ const w = safeWidth(width);
232
+ const units = graphemeUnits(value);
233
+ const positions = [{ offset: 0, row: 0, col: 0 }];
234
+ let row = 0;
235
+ let col = 0;
236
+
237
+ // Build every grapheme-boundary position in one pass. This is the same state
238
+ // transition as caretPosition(), but avoids re-segmenting and re-scanning the
239
+ // entire prefix for every candidate during mouse hit-testing.
240
+ for (let i = 0; i < units.length; i += 1) {
241
+ const { end, segment } = units[i];
242
+ if (segment === '\n') {
243
+ row += 1;
244
+ col = 0;
245
+ } else {
246
+ const segmentWidth = displayWidth(segment);
247
+ if (segmentWidth > 0) {
248
+ if (col > 0 && col + segmentWidth > w) {
249
+ row += 1;
250
+ col = 0;
251
+ }
252
+ col += segmentWidth;
253
+ // Decide the flush-boundary advance from the FULL text so a soft-wrap
254
+ // offset reports {row N+1, col 0} (matching ink), exactly as
255
+ // caretPosition(value, end, w) does when more content follows.
256
+ if (col === w && i < units.length - 1) {
257
+ row += 1;
258
+ col = 0;
259
+ }
260
+ }
261
+ }
262
+ positions.push({ offset: end, row, col });
263
+ }
244
264
  return positions;
245
265
  }
246
266
 
@@ -7,6 +7,23 @@ import { assistantBodyWidth, measureMarkdownTableRows } from './table-layout.mjs
7
7
  import { renderTokenAnsiSegments } from './render-ansi.mjs';
8
8
  import { resolveStreamingMarkdownParts } from './streaming-markdown.mjs';
9
9
 
10
+ // One latest measurement per live stream. The small LRU bound mirrors the
11
+ // streaming-markdown split caches: completed/abandoned stream ids cannot make
12
+ // this module retain an unbounded response history.
13
+ const streamingRowsByKey = new Map();
14
+ const STREAMING_ROWS_LRU_MAX = 32;
15
+
16
+ function cacheStreamingRows(key, entry) {
17
+ if (!key) return;
18
+ if (streamingRowsByKey.has(key)) streamingRowsByKey.delete(key);
19
+ streamingRowsByKey.set(key, entry);
20
+ while (streamingRowsByKey.size > STREAMING_ROWS_LRU_MAX) {
21
+ const oldest = streamingRowsByKey.keys().next().value;
22
+ if (oldest === undefined) break;
23
+ streamingRowsByKey.delete(oldest);
24
+ }
25
+ }
26
+
10
27
  function wrappedLineRows(line, width) {
11
28
  const text = String(line);
12
29
  const full = displayWidth(text);
@@ -62,17 +79,104 @@ export function measureMarkdownRenderedRows(text, columns, { trimPartialFences =
62
79
  return Math.max(1, rows);
63
80
  }
64
81
 
65
- export function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
82
+ function measureStreamingPartsUncached(parts, columns) {
83
+ if (parts.plain) {
84
+ return estimateWrappedRowsFallback(parts.unstableForRender, columns, 3);
85
+ }
86
+ let rows = 0;
87
+ let childCount = 0;
88
+ if (parts.stablePrefix) {
89
+ rows += measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
90
+ childCount += 1;
91
+ }
92
+ if (parts.unstableSuffix) {
93
+ rows += measureMarkdownRenderedRows(parts.unstableForRender, columns, { trimPartialFences: true });
94
+ childCount += 1;
95
+ }
96
+ if (childCount === 2) rows += 1;
97
+ return childCount === 0 ? 1 : Math.max(1, rows);
98
+ }
99
+
100
+ // Test/reference path: resolve the same renderer split, but deliberately bypass
101
+ // streamingRowsByKey and remeasure every rendered child from scratch.
102
+ export function measureStreamingMarkdownRenderedRowsUncached(text, columns, streamKey) {
66
103
  const value = String(text ?? '');
67
104
  if (!value) return 1;
105
+ return measureStreamingPartsUncached(resolveStreamingMarkdownParts(value, streamKey), columns);
106
+ }
107
+
108
+ export function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
109
+ const value = String(text ?? '');
110
+ const key = streamKey == null || streamKey === '' ? null : String(streamKey);
111
+ const cached = key ? streamingRowsByKey.get(key) : null;
112
+ // Check the exact render inputs before even resolving markdown parts. App
113
+ // renders caused by typing or overlays therefore do no wrapping/token work
114
+ // when the live tail itself did not change.
115
+ if (cached && cached.text === value && cached.columns === columns) {
116
+ cacheStreamingRows(key, cached);
117
+ return cached.rows;
118
+ }
119
+ if (!value) {
120
+ cacheStreamingRows(key, {
121
+ text: value,
122
+ columns,
123
+ mode: 'empty',
124
+ rows: 1,
125
+ });
126
+ return 1;
127
+ }
68
128
  const parts = resolveStreamingMarkdownParts(value, streamKey);
69
129
  if (parts.plain) {
70
- return estimateWrappedRowsFallback(parts.unstableForRender, columns, 3);
130
+ const plain = parts.unstableForRender;
131
+ const width = Math.max(8, Number(columns || 80) - 3);
132
+ const lastBreak = plain.lastIndexOf('\n');
133
+ const stablePrefix = lastBreak >= 0 ? plain.substring(0, lastBreak + 1) : '';
134
+ let stableRows = 0;
135
+ if (cached
136
+ && cached.mode === 'plain'
137
+ && cached.columns === columns
138
+ && stablePrefix.startsWith(cached.stablePrefix)) {
139
+ // Lines preceding the currently-growing final line wrap independently.
140
+ // Measure only complete lines added since the previous split.
141
+ const addedComplete = plain.substring(cached.stablePrefix.length, Math.max(cached.stablePrefix.length, lastBreak));
142
+ stableRows = cached.stableRows;
143
+ if (addedComplete) {
144
+ stableRows += addedComplete
145
+ .split('\n')
146
+ .reduce((sum, line) => sum + wrappedLineRows(line, width), 0);
147
+ }
148
+ } else if (lastBreak >= 0) {
149
+ stableRows = plain
150
+ .substring(0, lastBreak)
151
+ .split('\n')
152
+ .reduce((sum, line) => sum + wrappedLineRows(line, width), 0);
153
+ }
154
+ const finalLine = lastBreak >= 0 ? plain.substring(lastBreak + 1) : plain;
155
+ const rows = Math.max(1, stableRows + wrappedLineRows(finalLine, width));
156
+ cacheStreamingRows(key, {
157
+ text: value,
158
+ columns,
159
+ mode: 'plain',
160
+ stablePrefix,
161
+ stableRows,
162
+ rows,
163
+ });
164
+ return rows;
71
165
  }
72
166
  let rows = 0;
73
167
  let childCount = 0;
168
+ let stableRows = 0;
74
169
  if (parts.stablePrefix) {
75
- rows += measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
170
+ // The renderer mounts stablePrefix and unstableSuffix as separate Markdown
171
+ // children. An unchanged stable child is therefore safe to reuse exactly;
172
+ // only the independently-rendered live suffix needs measuring on append.
173
+ stableRows = cached
174
+ && cached.mode === 'markdown'
175
+ && cached.columns === columns
176
+ && cached.stablePrefix === parts.stablePrefix
177
+ ? cached.stableRows
178
+ : measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
179
+ rows += stableRows;
76
180
  childCount += 1;
77
181
  }
78
182
  if (parts.unstableSuffix) {
@@ -80,6 +184,14 @@ export function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
80
184
  childCount += 1;
81
185
  }
82
186
  if (childCount === 2) rows += 1;
83
- if (childCount === 0) return 1;
84
- return Math.max(1, rows);
187
+ const measuredRows = childCount === 0 ? 1 : Math.max(1, rows);
188
+ cacheStreamingRows(key, {
189
+ text: value,
190
+ columns,
191
+ mode: 'markdown',
192
+ stablePrefix: parts.stablePrefix,
193
+ stableRows,
194
+ rows: measuredRows,
195
+ });
196
+ return measuredRows;
85
197
  }
@@ -403,7 +403,7 @@ export default class Ink {
403
403
  // [mixdog fork] Update the mouse drag-selection rectangle and repaint so the
404
404
  // inverse highlight tracks the drag. Called by the App's mouse handler.
405
405
  // A no-op-equal update is skipped to avoid redundant frames during motion.
406
- setSelection = (rect, options = {}) => {
406
+ setSelection = (rect) => {
407
407
  const a = this.selectionRect;
408
408
  const same = a === rect ||
409
409
  (a && rect &&
@@ -417,21 +417,10 @@ export default class Ink {
417
417
  a.captureText === rect.captureText &&
418
418
  a.selectionForeground === rect.selectionForeground &&
419
419
  a.selectionBackground === rect.selectionBackground);
420
- if (same) {
421
- if (!options.immediate) {
422
- return;
423
- }
424
- }
425
- else {
426
- this.selectionRect = rect ?? null;
427
- }
420
+ if (same)
421
+ return;
422
+ this.selectionRect = rect ?? null;
428
423
  if (!this.isUnmounted) {
429
- if (options.immediate) {
430
- this.selectionRepaintEpoch++;
431
- this.selectionRepaintFlushPending = false;
432
- this.rootNode.onImmediateRender();
433
- return;
434
- }
435
424
  this.scheduleSelectionRepaint();
436
425
  }
437
426
  };
@@ -456,7 +445,10 @@ export default class Ink {
456
445
  if (this.hasPendingThrottledRender) {
457
446
  return;
458
447
  }
459
- this.rootNode.onImmediateRender();
448
+ // Selection motion must share Ink's normal maxFps throttle; the old
449
+ // unthrottled callback regenerated a whole frame for every accepted
450
+ // drag update even though selection updates are coalesced.
451
+ this.rootNode.onRender();
460
452
  });
461
453
  };
462
454
  // [mixdog fork] Given a 0-based cell (x, y), return the inclusive rect of the