mixdog 0.9.64 → 0.9.66

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.mjs +2 -2
  3. package/src/cli.mjs +1 -1
  4. package/src/repl.mjs +72 -6
  5. package/src/runtime/agent/orchestrator/session/context-utils.mjs +76 -21
  6. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +45 -4
  7. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +11 -1
  8. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +36 -13
  9. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +79 -18
  10. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -0
  11. package/src/runtime/agent/orchestrator/session/store.mjs +37 -2
  12. package/src/runtime/shared/turn-snapshot.mjs +184 -0
  13. package/src/session-runtime/context-status.mjs +2 -1
  14. package/src/session-runtime/provider-request-tools.mjs +6 -0
  15. package/src/session-runtime/runtime-core.mjs +4 -0
  16. package/src/session-runtime/session-turn-api.mjs +7 -0
  17. package/src/session-runtime/tool-catalog-schema.mjs +5 -1
  18. package/src/tui/App.jsx +1 -1
  19. package/src/tui/app/transcript-window.mjs +16 -0
  20. package/src/tui/app/use-transcript-window.mjs +36 -1
  21. package/src/tui/components/Markdown.jsx +15 -1
  22. package/src/tui/components/Message.jsx +1 -1
  23. package/src/tui/components/PromptInput.jsx +7 -0
  24. package/src/tui/dist/index.mjs +431 -81
  25. package/src/tui/engine/frame-batched-store.mjs +5 -3
  26. package/src/tui/engine/live-share.mjs +100 -0
  27. package/src/tui/engine/render-timing.mjs +28 -0
  28. package/src/tui/engine/session-api-ext.mjs +10 -0
  29. package/src/tui/engine/tui-steering-persist.mjs +25 -4
  30. package/src/tui/engine.mjs +36 -1
  31. package/src/tui/index.jsx +11 -3
  32. package/src/tui/markdown/measure-rendered-rows.mjs +28 -16
  33. package/src/tui/markdown/render-ansi.mjs +34 -1
  34. package/src/tui/markdown/stream-fence.mjs +44 -7
  35. package/src/tui/markdown/streaming-markdown.mjs +95 -27
  36. package/src/ui/stream-finalize.mjs +45 -0
@@ -55,6 +55,8 @@ export function trimPartialClosingFences(tokens) {
55
55
  */
56
56
  const OPEN_FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
57
57
  const CLOSE_FENCE_RE = /^ {0,3}([`~]+)[ \t]*$/;
58
+ const OPEN_FENCE_SCAN_LRU_MAX = 32;
59
+ const openFenceScanByStreamKey = new Map();
58
60
 
59
61
  function isClosingFence(line, char, openLen) {
60
62
  const m = CLOSE_FENCE_RE.exec(line);
@@ -66,12 +68,14 @@ function isClosingFence(line, char, openLen) {
66
68
  return run.startsWith(char.repeat(openLen));
67
69
  }
68
70
 
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++) {
71
+ function scanOpenFence(value, startAt = 0, initialOpen = null) {
72
+ let open = initialOpen;
73
+ let start = startAt;
74
+ const checkpointIndex = value.lastIndexOf('\n') + 1;
75
+ let openBeforeCheckpoint = start === checkpointIndex ? open : null;
76
+ for (let i = startAt; i <= value.length; i++) {
74
77
  if (i !== value.length && value[i] !== '\n') continue;
78
+ if (start === checkpointIndex) openBeforeCheckpoint = open;
75
79
  const line = value.slice(start, i);
76
80
  if (!open) {
77
81
  const m = OPEN_FENCE_RE.exec(line);
@@ -88,7 +92,40 @@ export function findOpenFenceStart(text) {
88
92
  }
89
93
  start = i + 1;
90
94
  }
95
+ return { open, checkpointIndex, openBeforeCheckpoint };
96
+ }
97
+
98
+ function touchOpenFenceScan(key, entry) {
99
+ if (openFenceScanByStreamKey.has(key)) openFenceScanByStreamKey.delete(key);
100
+ openFenceScanByStreamKey.set(key, entry);
101
+ while (openFenceScanByStreamKey.size > OPEN_FENCE_SCAN_LRU_MAX) {
102
+ const oldest = openFenceScanByStreamKey.keys().next().value;
103
+ if (oldest === undefined) break;
104
+ openFenceScanByStreamKey.delete(oldest);
105
+ }
106
+ }
107
+
108
+ export function resetOpenFenceScan(streamKey) {
109
+ if (streamKey == null || streamKey === '') return;
110
+ openFenceScanByStreamKey.delete(String(streamKey));
111
+ }
112
+
113
+ export function resetAllOpenFenceScans() {
114
+ openFenceScanByStreamKey.clear();
115
+ }
116
+
117
+ export function findOpenFenceStart(text, streamKey = null) {
118
+ const value = String(text ?? '');
119
+ const key = streamKey == null || streamKey === '' ? null : String(streamKey);
120
+ const cached = key ? openFenceScanByStreamKey.get(key) : null;
121
+ if (cached?.text === value) return cached.result;
122
+ const scanned = cached && value.startsWith(cached.text)
123
+ ? scanOpenFence(value, cached.checkpointIndex, cached.openBeforeCheckpoint)
124
+ : scanOpenFence(value);
91
125
  // 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 };
126
+ const result = !scanned.open || scanned.open.indent !== 0
127
+ ? null
128
+ : { index: scanned.open.index, lang: scanned.open.lang };
129
+ if (key) touchOpenFenceScan(key, { text: value, ...scanned, result });
130
+ return result;
94
131
  }
@@ -4,10 +4,17 @@
4
4
  */
5
5
  import { marked } from 'marked';
6
6
  import { configureMarked, hasMarkdownSyntax } from './render-ansi.mjs';
7
- import { trimPartialClosingFences, findOpenFenceStart } from './stream-fence.mjs';
7
+ import {
8
+ trimPartialClosingFences,
9
+ findOpenFenceStart,
10
+ resetAllOpenFenceScans,
11
+ resetOpenFenceScan,
12
+ } from './stream-fence.mjs';
8
13
  import { displayWidth } from '../display-width.mjs';
9
14
 
10
15
  const stablePrefixByStreamKey = new Map();
16
+ const markdownSyntaxByStreamKey = new Map();
17
+ const plainWindowSyntaxByStreamKey = new Map();
11
18
  // Reuse the current normalized-text split across measure → render → harvest.
12
19
  const resolvedPartsByStreamKey = new Map();
13
20
  const STABLE_PREFIX_LRU_MAX = 32;
@@ -17,48 +24,92 @@ export function streamingLayoutText(text) {
17
24
  return String(text ?? '').replace(/^\n+|\n+$/g, '');
18
25
  }
19
26
 
20
- export function windowPlainStreamingText(text, columns, maxRows) {
27
+ export function windowPlainStreamingText(text, columns, maxRows, streamKey = null) {
21
28
  const value = streamingLayoutText(text);
22
29
  const rowBudget = Math.max(0, Math.floor(Number(maxRows) || 0));
23
- if (!value || rowBudget <= 0 || hasMarkdownSyntax(value)) return value;
24
- const lines = value.split('\n');
30
+ const key = streamKey == null || streamKey === '' ? null : String(streamKey);
31
+ if (!value || rowBudget <= 0 || cachedStreamingHasMarkdownSyntax(
32
+ plainWindowSyntaxByStreamKey,
33
+ value,
34
+ key,
35
+ )) return value;
25
36
  const width = Math.max(1, Math.floor(Number(columns) || 80));
26
37
  let rows = 0;
27
- let start = lines.length;
28
- while (start > 0) {
29
- const line = lines[start - 1];
38
+ let end = value.length;
39
+ let start = end;
40
+ while (end >= 0) {
41
+ const newline = value.lastIndexOf('\n', end - 1);
42
+ const lineStart = newline + 1;
43
+ const line = value.slice(lineStart, end);
30
44
  const lineRows = Math.max(1, Math.ceil(displayWidth(line) / width));
31
45
  if (rows > 0 && rows + lineRows > rowBudget) break;
32
46
  rows += lineRows;
33
- start -= 1;
34
- if (rows >= rowBudget) break;
47
+ start = lineStart;
48
+ if (rows >= rowBudget || newline < 0) break;
49
+ end = newline;
35
50
  }
36
- return start > 0 ? lines.slice(start).join('\n') : value;
51
+ return start > 0 ? value.slice(start) : value;
37
52
  }
38
53
 
39
54
  function isWhitespaceOnlyText(text) {
40
55
  return !String(text ?? '').trim();
41
56
  }
42
57
 
43
- function touchStablePrefixKey(key, value) {
58
+ function touchLruKey(cache, key, value) {
44
59
  if (!key) return;
45
- if (stablePrefixByStreamKey.has(key)) stablePrefixByStreamKey.delete(key);
46
- stablePrefixByStreamKey.set(key, value);
47
- while (stablePrefixByStreamKey.size > STABLE_PREFIX_LRU_MAX) {
48
- const oldest = stablePrefixByStreamKey.keys().next().value;
60
+ if (cache.has(key)) cache.delete(key);
61
+ cache.set(key, value);
62
+ while (cache.size > STABLE_PREFIX_LRU_MAX) {
63
+ const oldest = cache.keys().next().value;
49
64
  if (oldest === undefined) break;
50
- stablePrefixByStreamKey.delete(oldest);
65
+ cache.delete(oldest);
51
66
  }
52
67
  }
53
68
 
69
+ function touchStablePrefixKey(key, value) {
70
+ touchLruKey(stablePrefixByStreamKey, key, value);
71
+ }
72
+
54
73
  function getStablePrefixKey(key) {
55
- if (!key || !stablePrefixByStreamKey.has(key)) return '';
74
+ if (!key || !stablePrefixByStreamKey.has(key)) return { text: '', chunks: [] };
56
75
  const value = stablePrefixByStreamKey.get(key);
57
- stablePrefixByStreamKey.delete(key);
58
- stablePrefixByStreamKey.set(key, value);
76
+ touchStablePrefixKey(key, value);
59
77
  return value;
60
78
  }
61
79
 
80
+ function stableStateForText(text, previous) {
81
+ if (!text) return { text: '', chunks: [] };
82
+ if (text.startsWith(previous.text)) {
83
+ const appended = text.substring(previous.text.length);
84
+ return appended
85
+ ? { text, chunks: [...previous.chunks, appended] }
86
+ : previous;
87
+ }
88
+ return { text, chunks: [text] };
89
+ }
90
+
91
+ function cachedStreamingHasMarkdownSyntax(cache, text, key) {
92
+ if (!key) return hasMarkdownSyntax(text);
93
+ const previous = cache.get(key);
94
+ let value;
95
+ if (previous && text.startsWith(previous.text)) {
96
+ if (previous.value) {
97
+ value = true;
98
+ } else {
99
+ const lineStart = previous.text.lastIndexOf('\n') + 1;
100
+ value = hasMarkdownSyntax(text.substring(Math.max(0, lineStart - 1)));
101
+ }
102
+ } else {
103
+ value = hasMarkdownSyntax(text);
104
+ }
105
+ touchLruKey(cache, key, { text, value });
106
+ return value;
107
+ }
108
+
109
+ function streamingHasMarkdownSyntax(text, key) {
110
+ return cachedStreamingHasMarkdownSyntax(markdownSyntaxByStreamKey, text, key);
111
+ }
112
+
62
113
  function getResolvedPartsKey(key, text) {
63
114
  if (!key) return null;
64
115
  const entry = resolvedPartsByStreamKey.get(key);
@@ -136,12 +187,18 @@ export function resetStreamingMarkdownStablePrefix(streamKey) {
136
187
  if (streamKey == null || streamKey === '') return;
137
188
  const key = String(streamKey);
138
189
  stablePrefixByStreamKey.delete(key);
190
+ markdownSyntaxByStreamKey.delete(key);
191
+ plainWindowSyntaxByStreamKey.delete(key);
139
192
  resolvedPartsByStreamKey.delete(key);
193
+ resetOpenFenceScan(key);
140
194
  }
141
195
 
142
196
  export function resetAllStreamingMarkdownStablePrefixes() {
143
197
  stablePrefixByStreamKey.clear();
198
+ markdownSyntaxByStreamKey.clear();
199
+ plainWindowSyntaxByStreamKey.clear();
144
200
  resolvedPartsByStreamKey.clear();
201
+ resetAllOpenFenceScans();
145
202
  }
146
203
 
147
204
  export function resolveStreamingMarkdownParts(text, streamKey) {
@@ -155,42 +212,47 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
155
212
  return cacheResolvedPartsKey(key, t, {
156
213
  plain: true,
157
214
  stablePrefix: '',
215
+ stableChunks: [],
158
216
  unstableSuffix: '',
159
217
  unstableForRender: '',
160
218
  });
161
219
  }
162
220
 
163
- if (!hasMarkdownSyntax(t)) {
221
+ if (!streamingHasMarkdownSyntax(t, key)) {
164
222
  if (key) stablePrefixByStreamKey.delete(key);
165
223
  return cacheResolvedPartsKey(key, t, {
166
224
  plain: true,
167
225
  stablePrefix: '',
226
+ stableChunks: [],
168
227
  unstableSuffix: t,
169
228
  unstableForRender: t,
170
229
  });
171
230
  }
172
231
 
173
- let stablePrefix = key ? getStablePrefixKey(key) : '';
174
- if (!t.startsWith(stablePrefix)) {
175
- stablePrefix = '';
232
+ let stableState = key ? getStablePrefixKey(key) : { text: '', chunks: [] };
233
+ if (!t.startsWith(stableState.text)) {
234
+ stableState = { text: '', chunks: [] };
176
235
  }
236
+ let stablePrefix = stableState.text;
177
237
 
178
238
  // Open-fence fast path: never run marked.lexer on a growing unclosed code
179
239
  // block (its closing-fence regex never matches and backtracks over the whole
180
240
  // body every delta — the ~56ms/frame cost). Split cheaply at the fence line:
181
241
  // everything before it is settled markdown (lexed + cached once by the stable
182
242
  // <Markdown>), the open block is rendered flat until the closing fence lands.
183
- const open = findOpenFenceStart(t);
243
+ const open = findOpenFenceStart(t, key);
184
244
  if (open) {
185
245
  let openPrefix = t.substring(0, open.index);
186
246
  if (isWhitespaceOnlyText(openPrefix)) openPrefix = '';
187
- if (key && openPrefix) touchStablePrefixKey(key, openPrefix);
247
+ stableState = stableStateForText(openPrefix, stableState);
248
+ if (key && openPrefix) touchStablePrefixKey(key, stableState);
188
249
  else if (key) stablePrefixByStreamKey.delete(key);
189
250
  const unstableSuffix = t.substring(openPrefix.length);
190
251
  return cacheResolvedPartsKey(key, t, {
191
252
  plain: false,
192
253
  openFence: true,
193
254
  stablePrefix: openPrefix,
255
+ stableChunks: stableState.chunks,
194
256
  unstableSuffix,
195
257
  unstableForRender: unstableSuffix,
196
258
  });
@@ -214,20 +276,26 @@ export function resolveStreamingMarkdownParts(text, streamKey) {
214
276
  if (advance > 0) {
215
277
  stablePrefix = t.substring(0, boundary + advance);
216
278
  if (isWhitespaceOnlyText(stablePrefix)) stablePrefix = '';
217
- if (key && stablePrefix) touchStablePrefixKey(key, stablePrefix);
279
+ stableState = stableStateForText(stablePrefix, stableState);
280
+ if (key && stablePrefix) touchStablePrefixKey(key, stableState);
218
281
  else if (key && !stablePrefix) stablePrefixByStreamKey.delete(key);
219
282
  }
220
283
  } catch {
221
284
  stablePrefix = '';
285
+ stableState = { text: '', chunks: [] };
222
286
  if (key) stablePrefixByStreamKey.delete(key);
223
287
  }
224
288
 
225
- if (isWhitespaceOnlyText(stablePrefix)) stablePrefix = '';
289
+ if (isWhitespaceOnlyText(stablePrefix)) {
290
+ stablePrefix = '';
291
+ stableState = { text: '', chunks: [] };
292
+ }
226
293
 
227
294
  const unstableSuffix = t.substring(stablePrefix.length);
228
295
  return cacheResolvedPartsKey(key, t, {
229
296
  plain: false,
230
297
  stablePrefix,
298
+ stableChunks: stableState.chunks,
231
299
  unstableSuffix,
232
300
  unstableForRender: balanceStreamingMarkdown(unstableSuffix),
233
301
  });
@@ -0,0 +1,45 @@
1
+ import { visibleWidth } from './ansi.mjs';
2
+
3
+ /**
4
+ * Build a conservative in-place final-format patch for a streamed terminal
5
+ * block. Logical rows must map 1:1 to physical rows; wrapped or structurally
6
+ * changed output returns null so the caller can use the full-redraw fallback.
7
+ */
8
+ export function buildStreamFinalPatch(rawText, renderedText, { columns = 80 } = {}) {
9
+ const raw = String(rawText ?? '');
10
+ const rendered = String(renderedText ?? '');
11
+ if (raw.includes('\r') || rendered.includes('\r')) return null;
12
+ const rawLines = raw.split('\n');
13
+ const renderedLines = rendered.split('\n');
14
+ if (rawLines.length !== renderedLines.length) return null;
15
+
16
+ const width = Math.max(1, Math.floor(Number(columns) || 80));
17
+ for (let index = 0; index < rawLines.length; index += 1) {
18
+ if (visibleWidth(rawLines[index]) >= width || visibleWidth(renderedLines[index]) >= width) {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ const changedRows = [];
24
+ for (let index = 0; index < rawLines.length; index += 1) {
25
+ if (rawLines[index] !== renderedLines[index]) changedRows.push(index);
26
+ }
27
+ if (changedRows.length === 0) {
28
+ return { output: '', changedRows: 0, totalRows: rawLines.length };
29
+ }
30
+
31
+ let output = '\r';
32
+ for (let index = rawLines.length - 1; index >= 0; index -= 1) {
33
+ if (rawLines[index] !== renderedLines[index]) {
34
+ output += `\x1b[2K${renderedLines[index]}`;
35
+ }
36
+ if (index > 0) output += '\r\x1b[1A';
37
+ }
38
+ if (rawLines.length > 1) {
39
+ output += `\r\x1b[${rawLines.length - 1}B`;
40
+ const lastWidth = visibleWidth(renderedLines.at(-1) || '');
41
+ if (lastWidth > 0) output += `\x1b[${lastWidth}C`;
42
+ }
43
+
44
+ return { output, changedRows: changedRows.length, totalRows: rawLines.length };
45
+ }