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
@@ -194,8 +194,8 @@ var require_mixdog_debug = __commonJS({
194
194
  import React21 from "react";
195
195
  import { render } from "../../../vendor/ink/build/index.js";
196
196
  import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync7, openSync as openSync2, readSync } from "node:fs";
197
- import { tmpdir as tmpdir2 } from "node:os";
198
- import { dirname as dirname9, join as join10 } from "node:path";
197
+ import { tmpdir as tmpdir3 } from "node:os";
198
+ import { dirname as dirname9, join as join11 } from "node:path";
199
199
  import { performance as performance3 } from "node:perf_hooks";
200
200
  import { format } from "node:util";
201
201
 
@@ -4671,14 +4671,32 @@ function offsetAtCell(text, row, col, width) {
4671
4671
  }
4672
4672
  function boundaryPositions(text, width) {
4673
4673
  const value = String(text || "");
4674
- const offsets = [0];
4675
- for (const unit of graphemeUnits(value)) offsets.push(unit.end);
4676
- const seen = /* @__PURE__ */ new Set();
4677
- const positions = offsets.filter((offset) => {
4678
- if (seen.has(offset)) return false;
4679
- seen.add(offset);
4680
- return true;
4681
- }).map((offset) => ({ offset, ...caretPosition(value, offset, width) }));
4674
+ const w = safeWidth(width);
4675
+ const units = graphemeUnits(value);
4676
+ const positions = [{ offset: 0, row: 0, col: 0 }];
4677
+ let row = 0;
4678
+ let col = 0;
4679
+ for (let i = 0; i < units.length; i += 1) {
4680
+ const { end, segment } = units[i];
4681
+ if (segment === "\n") {
4682
+ row += 1;
4683
+ col = 0;
4684
+ } else {
4685
+ const segmentWidth = displayWidth(segment);
4686
+ if (segmentWidth > 0) {
4687
+ if (col > 0 && col + segmentWidth > w) {
4688
+ row += 1;
4689
+ col = 0;
4690
+ }
4691
+ col += segmentWidth;
4692
+ if (col === w && i < units.length - 1) {
4693
+ row += 1;
4694
+ col = 0;
4695
+ }
4696
+ }
4697
+ }
4698
+ positions.push({ offset: end, row, col });
4699
+ }
4682
4700
  return positions;
4683
4701
  }
4684
4702
  function verticalOffset(text, offset, width, direction, preferredColumn = null) {
@@ -4919,11 +4937,14 @@ function PromptInput({
4919
4937
  const commitDraft = (next, options = {}) => {
4920
4938
  const sameDraft = draftStateEqual(draftRef.current, next);
4921
4939
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
4922
- if (sameDraft) return;
4940
+ if (sameDraft) {
4941
+ if (options.immediateSettle) scheduleImmediateFlush();
4942
+ return;
4943
+ }
4923
4944
  if (!options.skipHistory) recordUndoSnapshot(draftRef.current, next, options);
4924
4945
  draftRef.current = next;
4925
4946
  setDraft(next);
4926
- scheduleImmediateFlush();
4947
+ if (!options.throttledRender) scheduleImmediateFlush();
4927
4948
  if (next.value !== lastReportedValueRef.current) {
4928
4949
  lastReportedValueRef.current = next.value;
4929
4950
  onDraftChange?.(next.value);
@@ -5024,13 +5045,13 @@ function PromptInput({
5024
5045
  const queueMouseExtendCommit = (next, immediate = false) => {
5025
5046
  if (immediate) {
5026
5047
  cancelMouseExtendCoalesce();
5027
- commitDraft(next);
5048
+ commitDraft(next, { immediateSettle: true });
5028
5049
  mouseExtendCoalesceRef.current.t = Date.now();
5029
5050
  return;
5030
5051
  }
5031
5052
  if (draftStateEqual(draftRef.current, next)) {
5032
5053
  cancelMouseExtendCoalesce();
5033
- commitDraft(next);
5054
+ commitDraft(next, { throttledRender: true });
5034
5055
  return;
5035
5056
  }
5036
5057
  const state = mouseExtendCoalesceRef.current;
@@ -5040,7 +5061,7 @@ function PromptInput({
5040
5061
  if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
5041
5062
  cancelMouseExtendCoalesce();
5042
5063
  state.t = now;
5043
- commitDraft(next);
5064
+ commitDraft(next, { throttledRender: true });
5044
5065
  return;
5045
5066
  }
5046
5067
  if (state.timer) return;
@@ -5050,7 +5071,7 @@ function PromptInput({
5050
5071
  current.timer = null;
5051
5072
  current.pendingNext = null;
5052
5073
  current.t = Date.now();
5053
- if (pending) commitDraft(pending);
5074
+ if (pending) commitDraft(pending, { throttledRender: true });
5054
5075
  }, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
5055
5076
  state.timer.unref?.();
5056
5077
  };
@@ -6630,6 +6651,7 @@ import stringWidth6 from "string-width";
6630
6651
  // src/tui/app/text-layout.mjs
6631
6652
  import wrapAnsi from "wrap-ansi";
6632
6653
  var graphemeSegmenter3 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
6654
+ var promptRowsCache = null;
6633
6655
  function wrappedTextRows(value, width) {
6634
6656
  const w = Math.max(1, Math.floor(Number(width) || 1));
6635
6657
  let row = 0;
@@ -6657,7 +6679,14 @@ function wrappedTextRows(value, width) {
6657
6679
  return row + 1;
6658
6680
  }
6659
6681
  function promptContentRows(value, contentColumns) {
6660
- return wrappedTextRows(`${String(value ?? "")} `, contentColumns);
6682
+ const text = String(value ?? "");
6683
+ const width = Math.max(1, Math.floor(Number(contentColumns) || 1));
6684
+ if (promptRowsCache?.text === text && promptRowsCache.width === width) {
6685
+ return promptRowsCache.rows;
6686
+ }
6687
+ const rows = wrappedTextRows(`${text} `, width);
6688
+ promptRowsCache = { text, width, rows };
6689
+ return rows;
6661
6690
  }
6662
6691
  function wrappedDetailRows(text, width) {
6663
6692
  const value = String(text ?? "").trim();
@@ -9539,6 +9568,18 @@ function resolveStreamingMarkdownParts(text, streamKey) {
9539
9568
  }
9540
9569
 
9541
9570
  // src/tui/markdown/measure-rendered-rows.mjs
9571
+ var streamingRowsByKey = /* @__PURE__ */ new Map();
9572
+ var STREAMING_ROWS_LRU_MAX = 32;
9573
+ function cacheStreamingRows(key, entry) {
9574
+ if (!key) return;
9575
+ if (streamingRowsByKey.has(key)) streamingRowsByKey.delete(key);
9576
+ streamingRowsByKey.set(key, entry);
9577
+ while (streamingRowsByKey.size > STREAMING_ROWS_LRU_MAX) {
9578
+ const oldest = streamingRowsByKey.keys().next().value;
9579
+ if (oldest === void 0) break;
9580
+ streamingRowsByKey.delete(oldest);
9581
+ }
9582
+ }
9542
9583
  function wrappedLineRows(line, width) {
9543
9584
  const text = String(line);
9544
9585
  const full = displayWidth(text);
@@ -9600,15 +9641,55 @@ function measureMarkdownRenderedRows(text, columns, { trimPartialFences = false
9600
9641
  }
9601
9642
  function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
9602
9643
  const value = String(text ?? "");
9603
- if (!value) return 1;
9644
+ const key = streamKey == null || streamKey === "" ? null : String(streamKey);
9645
+ const cached = key ? streamingRowsByKey.get(key) : null;
9646
+ if (cached && cached.text === value && cached.columns === columns) {
9647
+ cacheStreamingRows(key, cached);
9648
+ return cached.rows;
9649
+ }
9650
+ if (!value) {
9651
+ cacheStreamingRows(key, {
9652
+ text: value,
9653
+ columns,
9654
+ mode: "empty",
9655
+ rows: 1
9656
+ });
9657
+ return 1;
9658
+ }
9604
9659
  const parts = resolveStreamingMarkdownParts(value, streamKey);
9605
9660
  if (parts.plain) {
9606
- return estimateWrappedRowsFallback(parts.unstableForRender, columns, 3);
9661
+ const plain = parts.unstableForRender;
9662
+ const width = Math.max(8, Number(columns || 80) - 3);
9663
+ const lastBreak = plain.lastIndexOf("\n");
9664
+ const stablePrefix = lastBreak >= 0 ? plain.substring(0, lastBreak + 1) : "";
9665
+ let stableRows2 = 0;
9666
+ if (cached && cached.mode === "plain" && cached.columns === columns && stablePrefix.startsWith(cached.stablePrefix)) {
9667
+ const addedComplete = plain.substring(cached.stablePrefix.length, Math.max(cached.stablePrefix.length, lastBreak));
9668
+ stableRows2 = cached.stableRows;
9669
+ if (addedComplete) {
9670
+ stableRows2 += addedComplete.split("\n").reduce((sum, line) => sum + wrappedLineRows(line, width), 0);
9671
+ }
9672
+ } else if (lastBreak >= 0) {
9673
+ stableRows2 = plain.substring(0, lastBreak).split("\n").reduce((sum, line) => sum + wrappedLineRows(line, width), 0);
9674
+ }
9675
+ const finalLine = lastBreak >= 0 ? plain.substring(lastBreak + 1) : plain;
9676
+ const rows2 = Math.max(1, stableRows2 + wrappedLineRows(finalLine, width));
9677
+ cacheStreamingRows(key, {
9678
+ text: value,
9679
+ columns,
9680
+ mode: "plain",
9681
+ stablePrefix,
9682
+ stableRows: stableRows2,
9683
+ rows: rows2
9684
+ });
9685
+ return rows2;
9607
9686
  }
9608
9687
  let rows = 0;
9609
9688
  let childCount = 0;
9689
+ let stableRows = 0;
9610
9690
  if (parts.stablePrefix) {
9611
- rows += measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
9691
+ stableRows = cached && cached.mode === "markdown" && cached.columns === columns && cached.stablePrefix === parts.stablePrefix ? cached.stableRows : measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
9692
+ rows += stableRows;
9612
9693
  childCount += 1;
9613
9694
  }
9614
9695
  if (parts.unstableSuffix) {
@@ -9616,8 +9697,16 @@ function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
9616
9697
  childCount += 1;
9617
9698
  }
9618
9699
  if (childCount === 2) rows += 1;
9619
- if (childCount === 0) return 1;
9620
- return Math.max(1, rows);
9700
+ const measuredRows = childCount === 0 ? 1 : Math.max(1, rows);
9701
+ cacheStreamingRows(key, {
9702
+ text: value,
9703
+ columns,
9704
+ mode: "markdown",
9705
+ stablePrefix: parts.stablePrefix,
9706
+ stableRows,
9707
+ rows: measuredRows
9708
+ });
9709
+ return measuredRows;
9621
9710
  }
9622
9711
 
9623
9712
  // src/runtime/shared/err-text.mjs
@@ -10573,7 +10662,7 @@ function lowerBound(values, target) {
10573
10662
  let hi = values.length;
10574
10663
  while (lo < hi) {
10575
10664
  const mid = Math.floor((lo + hi) / 2);
10576
- if (values[mid] < target) lo = mid + 1;
10665
+ if (transcriptRowAt(values, mid) < target) lo = mid + 1;
10577
10666
  else hi = mid;
10578
10667
  }
10579
10668
  return lo;
@@ -10583,14 +10672,14 @@ function upperBound(values, target) {
10583
10672
  let hi = values.length;
10584
10673
  while (lo < hi) {
10585
10674
  const mid = Math.floor((lo + hi) / 2);
10586
- if (values[mid] <= target) lo = mid + 1;
10675
+ if (transcriptRowAt(values, mid) <= target) lo = mid + 1;
10587
10676
  else hi = mid;
10588
10677
  }
10589
10678
  return lo;
10590
10679
  }
10591
10680
  function resolveAnchorScrollOffset({ anchor, items, curPrefix, totalRows, viewRows, maxRows }) {
10592
10681
  if (!anchor || anchor.id == null) return null;
10593
- if (!Array.isArray(curPrefix) || curPrefix.length <= 1) return null;
10682
+ if (!curPrefix || curPrefix.length <= 1) return null;
10594
10683
  const list = Array.isArray(items) ? items : [];
10595
10684
  let idx = -1;
10596
10685
  for (let i = list.length - 1; i >= 0; i--) {
@@ -10600,9 +10689,9 @@ function resolveAnchorScrollOffset({ anchor, items, curPrefix, totalRows, viewRo
10600
10689
  }
10601
10690
  }
10602
10691
  if (idx < 0 || idx > curPrefix.length - 2) return null;
10603
- const itemHeight = Math.max(0, (curPrefix[idx + 1] || 0) - (curPrefix[idx] || 0));
10692
+ const itemHeight = Math.max(0, transcriptRowAt(curPrefix, idx + 1) - transcriptRowAt(curPrefix, idx));
10604
10693
  const clampedOffset = Math.max(0, Math.min(Number(anchor.offset) || 0, itemHeight));
10605
- const anchorRowCur = (curPrefix[idx] || 0) + clampedOffset;
10694
+ const anchorRowCur = transcriptRowAt(curPrefix, idx) + clampedOffset;
10606
10695
  return Math.max(0, Math.min(maxRows, totalRows - viewRows - anchorRowCur));
10607
10696
  }
10608
10697
  function fnv1a32(str) {
@@ -10662,8 +10751,21 @@ var transcriptRowsCache = /* @__PURE__ */ new WeakMap();
10662
10751
  var transcriptMeasuredRowsCache = /* @__PURE__ */ new WeakMap();
10663
10752
  var streamingMeasuredRowsById = /* @__PURE__ */ new Map();
10664
10753
  var streamingEstimateHighWaterById = /* @__PURE__ */ new Map();
10754
+ var streamingTailEstimateById = /* @__PURE__ */ new Map();
10755
+ var STREAMING_TAIL_ESTIMATE_LRU_MAX = 8;
10756
+ function cacheStreamingTailEstimate(id, entry) {
10757
+ if (id == null) return;
10758
+ if (streamingTailEstimateById.has(id)) streamingTailEstimateById.delete(id);
10759
+ streamingTailEstimateById.set(id, entry);
10760
+ while (streamingTailEstimateById.size > STREAMING_TAIL_ESTIMATE_LRU_MAX) {
10761
+ const oldest = streamingTailEstimateById.keys().next().value;
10762
+ if (oldest === void 0) break;
10763
+ streamingTailEstimateById.delete(oldest);
10764
+ streamingEstimateHighWaterById.delete(oldest);
10765
+ }
10766
+ }
10665
10767
  function hasStreamingRowStateToPrune() {
10666
- return streamingMeasuredRowsById.size > 0 || streamingEstimateHighWaterById.size > 0;
10768
+ return streamingMeasuredRowsById.size > 0 || streamingEstimateHighWaterById.size > 0 || streamingTailEstimateById.size > 0;
10667
10769
  }
10668
10770
  function pruneStreamingMeasuredRowsById(liveIds) {
10669
10771
  if (!liveIds) return;
@@ -10677,6 +10779,11 @@ function pruneStreamingMeasuredRowsById(liveIds) {
10677
10779
  if (!liveIds.has(id)) streamingEstimateHighWaterById.delete(id);
10678
10780
  }
10679
10781
  }
10782
+ if (streamingTailEstimateById.size > 0) {
10783
+ for (const id of streamingTailEstimateById.keys()) {
10784
+ if (!liveIds.has(id)) streamingTailEstimateById.delete(id);
10785
+ }
10786
+ }
10680
10787
  }
10681
10788
  function carryTranscriptMeasuredRowsCache(prevItem, nextItem) {
10682
10789
  if (!TRANSCRIPT_MEASURED_ROWS || !prevItem || !nextItem || prevItem === nextItem) return;
@@ -10707,20 +10814,42 @@ function assistantTextForStreamingRowEstimate(text) {
10707
10814
  return streamingLayoutText(text);
10708
10815
  }
10709
10816
  function streamingEstimateRows(item, columns, toolOutputExpanded) {
10817
+ const id = item?.id;
10818
+ const exactText = String(item?.text ?? "");
10819
+ const toolExpanded = toolOutputExpanded ? 1 : 0;
10820
+ const renderMode = item?.kind === "assistant" && item?.streaming ? "assistant-streaming-markdown" : "other";
10821
+ const cached = id == null ? null : streamingTailEstimateById.get(id);
10822
+ if (cached && cached.text === exactText && cached.columns === columns && cached.toolExpanded === toolExpanded && cached.renderMode === renderMode) {
10823
+ cacheStreamingTailEstimate(id, cached);
10824
+ return cached.rows;
10825
+ }
10710
10826
  const trimmedText = assistantTextForStreamingRowEstimate(item.text);
10711
10827
  const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
10712
10828
  const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
10713
10829
  const quantized = Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
10714
- const id = item?.id;
10715
10830
  if (id == null) return quantized;
10716
- const toolExpanded = toolOutputExpanded ? 1 : 0;
10717
10831
  const prev = streamingEstimateHighWaterById.get(id);
10718
- if (!prev || prev.columns !== columns || prev.toolExpanded !== toolExpanded) {
10719
- streamingEstimateHighWaterById.set(id, { rows: quantized, columns, toolExpanded });
10720
- return quantized;
10832
+ let rows;
10833
+ if (!prev || prev.columns !== columns || prev.toolExpanded !== toolExpanded || prev.renderMode !== renderMode) {
10834
+ streamingEstimateHighWaterById.set(id, {
10835
+ rows: quantized,
10836
+ columns,
10837
+ toolExpanded,
10838
+ renderMode
10839
+ });
10840
+ rows = quantized;
10841
+ } else {
10842
+ if (quantized > prev.rows) prev.rows = quantized;
10843
+ rows = prev.rows;
10721
10844
  }
10722
- if (quantized > prev.rows) prev.rows = quantized;
10723
- return prev.rows;
10845
+ cacheStreamingTailEstimate(id, {
10846
+ text: exactText,
10847
+ columns,
10848
+ toolExpanded,
10849
+ renderMode,
10850
+ rows
10851
+ });
10852
+ return rows;
10724
10853
  }
10725
10854
  function streamingTailMountedGrowth(items, columns, toolOutputExpanded) {
10726
10855
  const list = Array.isArray(items) ? items : [];
@@ -10752,12 +10881,14 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
10752
10881
  if (idEntry) {
10753
10882
  streamingMeasuredRowsById.delete(item.id);
10754
10883
  streamingEstimateHighWaterById.delete(item.id);
10884
+ streamingTailEstimateById.delete(item.id);
10755
10885
  }
10756
10886
  return streamingEstimateRows(item, columns, toolOutputExpanded);
10757
10887
  }
10758
10888
  if (item.kind === "assistant") {
10759
10889
  if (streamingMeasuredRowsById.has(item.id)) streamingMeasuredRowsById.delete(item.id);
10760
10890
  if (streamingEstimateHighWaterById.has(item.id)) streamingEstimateHighWaterById.delete(item.id);
10891
+ if (streamingTailEstimateById.has(item.id)) streamingTailEstimateById.delete(item.id);
10761
10892
  }
10762
10893
  const variantKey = transcriptItemVariantKey(item);
10763
10894
  const toolExpanded = toolOutputExpanded ? 1 : 0;
@@ -10772,14 +10903,15 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
10772
10903
  function buildTranscriptRowIndex(items, {
10773
10904
  columns = 80,
10774
10905
  toolOutputExpanded = false,
10775
- suppressMeasuredRowHeights = false
10906
+ suppressMeasuredRowHeights = false,
10907
+ streamingTailItem = null
10776
10908
  } = {}) {
10777
10909
  const allItems = Array.isArray(items) ? items : [];
10778
10910
  const rows = new Array(allItems.length);
10779
10911
  const prefixRows = new Array(allItems.length + 1);
10780
10912
  prefixRows[0] = 0;
10781
10913
  for (let i = 0; i < allItems.length; i++) {
10782
- const item = allItems[i];
10914
+ const item = streamingTailItem && i === allItems.length - 1 ? streamingTailItem : allItems[i];
10783
10915
  const measured = suppressMeasuredRowHeights ? null : measuredTranscriptRows(item, columns, toolOutputExpanded);
10784
10916
  const rowCount = measured != null ? measured : estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded);
10785
10917
  rows[i] = rowCount;
@@ -10797,17 +10929,19 @@ function buildTranscriptRowIndexIncremental(items, {
10797
10929
  suppressMeasuredRowHeights = false,
10798
10930
  measuredRowsVersion = 0,
10799
10931
  cacheRef = null,
10800
- prefixRevision = null
10932
+ prefixRevision = null,
10933
+ streamingTailItem = null
10801
10934
  } = {}) {
10802
10935
  const allItems = Array.isArray(items) ? items : [];
10803
- const tail = trailingStreamingItem(allItems);
10936
+ const tail = streamingTailItem || trailingStreamingItem(allItems);
10804
10937
  const holder = cacheRef || { current: null };
10805
10938
  if (!tail) {
10806
10939
  holder.current = null;
10807
10940
  return buildTranscriptRowIndex(allItems, {
10808
10941
  columns,
10809
10942
  toolOutputExpanded,
10810
- suppressMeasuredRowHeights
10943
+ suppressMeasuredRowHeights,
10944
+ streamingTailItem
10811
10945
  });
10812
10946
  }
10813
10947
  const prefixLen = allItems.length - 1;
@@ -10816,17 +10950,19 @@ function buildTranscriptRowIndexIncremental(items, {
10816
10950
  {
10817
10951
  const tailMeasured = suppressMeasuredRowHeights ? null : measuredTranscriptRows(tail, columns, toolOutputExpanded);
10818
10952
  const tailRows = tailMeasured != null ? tailMeasured : estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
10819
- const rows = cache.prefixRowsArr.slice();
10820
- rows.push(tailRows);
10821
- const prefixRows = cache.prefixPrefixRows.slice();
10822
- prefixRows.push(cache.prefixTotal + tailRows);
10823
- return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
10953
+ const totalRows = cache.prefixTotal + tailRows;
10954
+ return {
10955
+ rows: appendTranscriptRow(cache.prefixRowsArr, tailRows),
10956
+ prefixRows: appendTranscriptRow(cache.prefixPrefixRows, totalRows),
10957
+ totalRows
10958
+ };
10824
10959
  }
10825
10960
  }
10826
10961
  const full = buildTranscriptRowIndex(allItems, {
10827
10962
  columns,
10828
10963
  toolOutputExpanded,
10829
- suppressMeasuredRowHeights
10964
+ suppressMeasuredRowHeights,
10965
+ streamingTailItem
10830
10966
  });
10831
10967
  holder.current = {
10832
10968
  columns,
@@ -10842,6 +10978,42 @@ function buildTranscriptRowIndexIncremental(items, {
10842
10978
  };
10843
10979
  return full;
10844
10980
  }
10981
+ function transcriptStructureSignature(items, columns, toolOutputExpanded, prefixRevision = 0, streamingTailItem = null) {
10982
+ const list = Array.isArray(items) ? items : [];
10983
+ const tail = streamingTailItem || trailingStreamingItem(list);
10984
+ const tailRows = tail ? estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded) : 0;
10985
+ return `${Math.max(0, Number(prefixRevision) || 0)}|${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}|${tail?.id ?? "_"}:${tailRows}`;
10986
+ }
10987
+ function transcriptRowAt(values, index) {
10988
+ if (!values || index < 0 || index >= values.length) return 0;
10989
+ return typeof values.atIndex === "function" ? values.atIndex(index) : Number(values[index]) || 0;
10990
+ }
10991
+ function appendTranscriptRow(prefix, tailValue) {
10992
+ return Object.freeze({
10993
+ length: prefix.length + 1,
10994
+ atIndex: (index) => index === prefix.length ? tailValue : Number(prefix[index]) || 0,
10995
+ slice: (start = 0, end = prefix.length + 1) => {
10996
+ const values = [];
10997
+ const lo = Math.max(0, start < 0 ? prefix.length + 1 + start : start);
10998
+ const hi = Math.min(prefix.length + 1, end < 0 ? prefix.length + 1 + end : end);
10999
+ for (let index = lo; index < hi; index++) {
11000
+ values.push(index === prefix.length ? tailValue : prefix[index]);
11001
+ }
11002
+ return values;
11003
+ }
11004
+ });
11005
+ }
11006
+ function transcriptItemsWithStableTail(settledItems, streamingTailItem, cacheRef) {
11007
+ const settled = Array.isArray(settledItems) ? settledItems : [];
11008
+ if (!streamingTailItem) return settled;
11009
+ const previous = cacheRef?.current;
11010
+ if (previous && previous.settled === settled && previous.tailId === streamingTailItem.id) {
11011
+ return previous.items;
11012
+ }
11013
+ const items = [...settled, streamingTailItem];
11014
+ if (cacheRef) cacheRef.current = { settled, tailId: streamingTailItem.id, items };
11015
+ return items;
11016
+ }
10845
11017
  function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24, columns = 80, toolOutputExpanded = false, rowIndex = null } = {}) {
10846
11018
  const allItems = Array.isArray(items) ? items : [];
10847
11019
  const itemCount = allItems.length;
@@ -10878,7 +11050,7 @@ function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24,
10878
11050
  endIndex = Math.min(itemCount, Math.max(visibleEndIndex, startIndex + effectiveMaxItems));
10879
11051
  if (endIndex - startIndex > effectiveMaxItems) startIndex = Math.max(0, endIndex - effectiveMaxItems);
10880
11052
  }
10881
- const bottomSpacerRows = Math.max(0, totalRows - (prefixRows[endIndex] || totalRows));
11053
+ const bottomSpacerRows = Math.max(0, totalRows - (transcriptRowAt(prefixRows, endIndex) || totalRows));
10882
11054
  return {
10883
11055
  startIndex,
10884
11056
  endIndex,
@@ -11016,6 +11188,9 @@ function useMouseInput({
11016
11188
  promptMouseSelectionRef.current?.clear?.();
11017
11189
  applySelectionRect(null);
11018
11190
  };
11191
+ const finishWindowsMouseGesture = () => {
11192
+ if (IS_WINDOWS_TERMINAL) store.forceRenderRepaint?.();
11193
+ };
11019
11194
  const EDGE_AUTOSCROLL_INTERVAL_MS = 50;
11020
11195
  const stopEdgeAutoscroll = () => {
11021
11196
  const st = edgeAutoscrollRef.current;
@@ -11089,6 +11264,7 @@ function useMouseInput({
11089
11264
  if (dragRef.current.active) {
11090
11265
  const last = dragRef.current.last || {};
11091
11266
  finalizeActiveDrag(Number(last.x) || 0, Number(last.y) || 0);
11267
+ finishWindowsMouseGesture();
11092
11268
  } else {
11093
11269
  stopEdgeAutoscroll();
11094
11270
  }
@@ -11123,9 +11299,6 @@ function useMouseInput({
11123
11299
  const shiftHeld = (button & MOUSE_SHIFT_MASK) !== 0;
11124
11300
  const ctrlHeld = (button & MOUSE_CTRL_MASK) !== 0;
11125
11301
  if (IS_WINDOWS_TERMINAL && shiftHeld) return;
11126
- if (IS_WINDOWS_TERMINAL && press && !isMotion) {
11127
- store.forceRenderRepaint?.();
11128
- }
11129
11302
  const extendHeld = shiftHeld || ctrlHeld;
11130
11303
  const isRightPress = baseButton === 2 && press && !isMotion;
11131
11304
  if (isRightPress) {
@@ -11138,6 +11311,7 @@ function useMouseInput({
11138
11311
  dragRef.current = { anchor: { x, y }, anchorScroll: 0, last: { x, y }, active: false, rect: null, region: "prompt", anchorSpan: null };
11139
11312
  if (offset != null) ctl.extendTo?.(offset, true);
11140
11313
  lastClickRef.current = { x: -1, y: -1, t: 0 };
11314
+ finishWindowsMouseGesture();
11141
11315
  }
11142
11316
  return;
11143
11317
  }
@@ -11155,6 +11329,7 @@ function useMouseInput({
11155
11329
  dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
11156
11330
  applySelectionRect(rect);
11157
11331
  lastClickRef.current = { x, y, t: nowR, count: 1 };
11332
+ finishWindowsMouseGesture();
11158
11333
  return;
11159
11334
  }
11160
11335
  if (dragRef.current.region === regionR && !dragRef.current.anchorSpan && dragRef.current.anchor && dragRef.current.rect && !(dragRef.current.rect.x1 === dragRef.current.rect.x2 && dragRef.current.rect.y1 === dragRef.current.rect.y2)) {
@@ -11166,6 +11341,7 @@ function useMouseInput({
11166
11341
  dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
11167
11342
  applySelectionRect(rect);
11168
11343
  lastClickRef.current = { x, y, t: nowR, count: 1 };
11344
+ finishWindowsMouseGesture();
11169
11345
  return;
11170
11346
  }
11171
11347
  return;
@@ -11208,6 +11384,7 @@ function useMouseInput({
11208
11384
  dragRef.current.region = null;
11209
11385
  dragRef.current.anchorSpan = null;
11210
11386
  clearAllSelections();
11387
+ finishWindowsMouseGesture();
11211
11388
  return;
11212
11389
  }
11213
11390
  const region = inTranscript ? "transcript" : "status";
@@ -11317,6 +11494,7 @@ function useMouseInput({
11317
11494
  }
11318
11495
  } else if (!press && baseButton === 0 && dragRef.current.active) {
11319
11496
  finalizeActiveDrag(x, y);
11497
+ finishWindowsMouseGesture();
11320
11498
  }
11321
11499
  }
11322
11500
  };
@@ -11330,6 +11508,73 @@ function useMouseInput({
11330
11508
 
11331
11509
  // src/tui/app/use-transcript-scroll.mjs
11332
11510
  import { useCallback as useCallback3, useEffect as useEffect8, useRef as useRef8 } from "react";
11511
+
11512
+ // src/tui/engine/render-timing.mjs
11513
+ var RENDER_ACK_HANG_GUARD_MS = 250;
11514
+ var RENDER_SETTLE_IDLE_MS = 64;
11515
+ var pendingRenderAcks = [];
11516
+ var renderAckSeq = 0;
11517
+ var scheduleRenderFrameAck = () => {
11518
+ const seq = ++renderAckSeq;
11519
+ setImmediate(() => notifyRenderFrame(seq));
11520
+ };
11521
+ var notifyRenderFrame = (seq = ++renderAckSeq) => {
11522
+ if (pendingRenderAcks.length === 0) return;
11523
+ const acks = pendingRenderAcks;
11524
+ pendingRenderAcks = [];
11525
+ for (const ack of acks) ack(seq);
11526
+ };
11527
+ var yieldToRenderer = ({ frames = 1 } = {}) => new Promise((resolve5) => {
11528
+ const minSeq = renderAckSeq;
11529
+ let remainingFrames = Math.max(1, Math.floor(Number(frames) || 1));
11530
+ let settled = false;
11531
+ let sawRealFrame = false;
11532
+ let timer2 = null;
11533
+ const finish = () => {
11534
+ if (settled) return;
11535
+ settled = true;
11536
+ if (timer2) clearTimeout(timer2);
11537
+ const idx = pendingRenderAcks.indexOf(onFrame);
11538
+ if (idx !== -1) pendingRenderAcks.splice(idx, 1);
11539
+ resolve5();
11540
+ };
11541
+ const onTimeout = () => {
11542
+ if (settled) return;
11543
+ if (!sawRealFrame && renderAckSeq > minSeq) {
11544
+ setImmediate(() => {
11545
+ if (!settled && !sawRealFrame) finish();
11546
+ });
11547
+ return;
11548
+ }
11549
+ finish();
11550
+ };
11551
+ const armWait = () => {
11552
+ if (settled) return;
11553
+ if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
11554
+ if (timer2) clearTimeout(timer2);
11555
+ timer2 = setTimeout(onTimeout, sawRealFrame ? RENDER_SETTLE_IDLE_MS : RENDER_ACK_HANG_GUARD_MS);
11556
+ };
11557
+ const onFrame = (seq = 0) => {
11558
+ if (settled) return;
11559
+ if (seq <= minSeq) {
11560
+ if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
11561
+ return;
11562
+ }
11563
+ sawRealFrame = true;
11564
+ if (timer2) {
11565
+ clearTimeout(timer2);
11566
+ timer2 = null;
11567
+ }
11568
+ const idx = pendingRenderAcks.indexOf(onFrame);
11569
+ if (idx !== -1) pendingRenderAcks.splice(idx, 1);
11570
+ remainingFrames -= 1;
11571
+ if (remainingFrames <= 0) finish();
11572
+ else armWait();
11573
+ };
11574
+ armWait();
11575
+ });
11576
+
11577
+ // src/tui/app/use-transcript-scroll.mjs
11333
11578
  function useTranscriptScroll({
11334
11579
  store,
11335
11580
  frameColumns,
@@ -11352,7 +11597,7 @@ function useTranscriptScroll({
11352
11597
  const scrollAnimationRef = useRef8(null);
11353
11598
  const selectionPaintRef = useRef8({ t: 0, rect: null, pending: null, timer: null });
11354
11599
  const scrollCoalesceRef = useRef8({ pendingRows: 0, timer: null });
11355
- const selectionTextTimerRef = useRef8(null);
11600
+ const selectionTextCaptureRef = useRef8(0);
11356
11601
  const stitchBufferRef = useRef8(/* @__PURE__ */ new Map());
11357
11602
  const stitchHarvestTimerRef = useRef8(null);
11358
11603
  const stitchHarvestScrollRef = useRef8(0);
@@ -11465,12 +11710,12 @@ function useTranscriptScroll({
11465
11710
  stopSmoothScroll();
11466
11711
  }, [stopSmoothScroll]);
11467
11712
  const rememberSelectionTextSoon = useCallback3(() => {
11468
- if (selectionTextTimerRef.current) return;
11469
- selectionTextTimerRef.current = setTimeout(() => {
11470
- selectionTextTimerRef.current = null;
11713
+ const capture = ++selectionTextCaptureRef.current;
11714
+ void yieldToRenderer().then(() => {
11715
+ if (capture !== selectionTextCaptureRef.current) return;
11471
11716
  const text = store.getRenderSelectionText?.();
11472
11717
  if (text && text.trim()) selectionTextRef.current = text;
11473
- }, 0);
11718
+ });
11474
11719
  }, [store]);
11475
11720
  const selectionClip = useCallback3(() => {
11476
11721
  if (dragRef.current.region === "status") {
@@ -11496,22 +11741,20 @@ function useTranscriptScroll({
11496
11741
  if (options.captureText === false) clipped.captureText = false;
11497
11742
  return clipped;
11498
11743
  }, [selectionClip]);
11499
- const paintSelectionRect = useCallback3((clippedRect, { rememberText = true, immediate = false } = {}) => {
11744
+ const paintSelectionRect = useCallback3((clippedRect, { rememberText = true } = {}) => {
11500
11745
  const nextRect = clippedRect || null;
11501
11746
  const state = selectionPaintRef.current;
11502
11747
  if (selectionRectsEqual(state.rect, nextRect)) {
11503
11748
  const needsCapture = nextRect && rememberText && nextRect.captureText !== false;
11504
- if (!immediate && !needsCapture) return false;
11505
- if (immediate || needsCapture) {
11506
- store.setRenderSelection?.(nextRect, { immediate: true });
11507
- }
11749
+ if (!needsCapture) return false;
11750
+ store.setRenderSelection?.(nextRect);
11508
11751
  if (needsCapture) rememberSelectionTextSoon();
11509
11752
  if (nextRect) harvestStitchRowsSoon();
11510
11753
  return true;
11511
11754
  }
11512
11755
  state.rect = nextRect;
11513
11756
  state.t = Date.now();
11514
- store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : void 0);
11757
+ store.setRenderSelection?.(nextRect);
11515
11758
  if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
11516
11759
  if (nextRect) harvestStitchRowsSoon();
11517
11760
  return true;
@@ -11533,17 +11776,18 @@ function useTranscriptScroll({
11533
11776
  state.timer = null;
11534
11777
  }
11535
11778
  state.pending = null;
11536
- if (pending) paintSelectionRect(pending, { rememberText: false, immediate: true });
11779
+ if (pending) paintSelectionRect(pending, { rememberText: false });
11537
11780
  }, [paintSelectionRect]);
11538
11781
  const applySelectionRect = useCallback3((rect) => {
11539
11782
  const clippedRect = withSelectionClip(rect);
11540
11783
  dragRef.current.rect = clippedRect || null;
11541
11784
  if (!clippedRect) {
11785
+ selectionTextCaptureRef.current += 1;
11542
11786
  selectionTextRef.current = "";
11543
11787
  clearStitchBuffer();
11544
11788
  }
11545
11789
  cancelPendingSelectionPaint();
11546
- paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
11790
+ paintSelectionRect(clippedRect, { rememberText: true });
11547
11791
  }, [paintSelectionRect, withSelectionClip, clearStitchBuffer, cancelPendingSelectionPaint]);
11548
11792
  const applySelectionRectThrottled = useCallback3((rect) => {
11549
11793
  const clippedRect = withSelectionClip(rect, { captureText: false });
@@ -11634,8 +11878,7 @@ function useTranscriptScroll({
11634
11878
  if (paintState.timer) clearTimeout(paintState.timer);
11635
11879
  paintState.timer = null;
11636
11880
  paintState.pending = null;
11637
- if (selectionTextTimerRef.current) clearTimeout(selectionTextTimerRef.current);
11638
- selectionTextTimerRef.current = null;
11881
+ selectionTextCaptureRef.current += 1;
11639
11882
  if (stitchHarvestTimerRef.current) clearTimeout(stitchHarvestTimerRef.current);
11640
11883
  stitchHarvestTimerRef.current = null;
11641
11884
  const coalesceState = scrollCoalesceRef.current;
@@ -11645,6 +11888,44 @@ function useTranscriptScroll({
11645
11888
  }, []);
11646
11889
  const scrollTranscriptRows = useCallback3((deltaRows, options = {}) => {
11647
11890
  const maxTarget = Math.max(0, Number(maxScrollRowsRef.current) || 0);
11891
+ const historyState = store.getState?.() || {};
11892
+ const captureRestoreAnchor = () => {
11893
+ const geom = transcriptGeomRef.current || {};
11894
+ const prefixRows = geom.prefixRows;
11895
+ if (!prefixRows || prefixRows.length <= 1) return;
11896
+ const total = Math.max(0, Number(geom.totalRows) || 0);
11897
+ const view = Math.max(1, Number(geom.viewRows) || 1);
11898
+ const target2 = Math.max(0, Number(scrollTargetRef.current) || 0);
11899
+ const row = Math.max(0, Math.min(total, total - target2 - view));
11900
+ let index = upperBound(prefixRows, row) - 1;
11901
+ index = Math.max(0, Math.min(prefixRows.length - 2, index));
11902
+ const item = geom.items?.[index];
11903
+ if (item?.id != null) {
11904
+ transcriptAnchorRef.current = {
11905
+ id: item.id,
11906
+ offset: Math.max(0, row - transcriptRowAt(prefixRows, index))
11907
+ };
11908
+ transcriptAnchorDirtyRef.current = false;
11909
+ }
11910
+ };
11911
+ if (deltaRows > 0 && scrollTargetRef.current >= maxTarget && historyState.transcriptHistoryBefore) {
11912
+ captureRestoreAnchor();
11913
+ if (!store.restoreOlderTranscript?.()) return;
11914
+ stopSmoothScroll();
11915
+ cancelTranscriptFollow();
11916
+ return;
11917
+ }
11918
+ if (deltaRows < 0 && scrollTargetRef.current <= 0 && historyState.transcriptHistoryAfter) {
11919
+ captureRestoreAnchor();
11920
+ if (!store.restoreNewerTranscript?.()) return;
11921
+ stopSmoothScroll();
11922
+ cancelTranscriptFollow();
11923
+ const anchoredTarget = Math.max(1, maxTarget);
11924
+ scrollTargetRef.current = anchoredTarget;
11925
+ scrollPositionRef.current = anchoredTarget;
11926
+ setScrollOffset(anchoredTarget);
11927
+ return;
11928
+ }
11648
11929
  const target = Math.max(0, Math.min(maxTarget, scrollTargetRef.current + deltaRows));
11649
11930
  const appliedDelta = target - scrollTargetRef.current;
11650
11931
  if (appliedDelta !== 0 && dragRef.current.region === "transcript" && dragRef.current.rect) {
@@ -11660,7 +11941,7 @@ function useTranscriptScroll({
11660
11941
  } else {
11661
11942
  const geom = transcriptGeomRef.current || {};
11662
11943
  const prefixRows = geom.prefixRows;
11663
- if (Array.isArray(prefixRows) && prefixRows.length > 1) {
11944
+ if (prefixRows && prefixRows.length > 1) {
11664
11945
  const gTotal = Math.max(0, Number(geom.totalRows) || 0);
11665
11946
  const gView = Math.max(1, Number(geom.viewRows) || 1);
11666
11947
  const anchorRow = Math.max(0, Math.min(gTotal, gTotal - target - gView));
@@ -11670,7 +11951,7 @@ function useTranscriptScroll({
11670
11951
  const items = geom.items || [];
11671
11952
  const anchorItem = items[idx];
11672
11953
  if (anchorItem && anchorItem.id != null) {
11673
- transcriptAnchorRef.current = { id: anchorItem.id, offset: Math.max(0, anchorRow - (prefixRows[idx] || 0)) };
11954
+ transcriptAnchorRef.current = { id: anchorItem.id, offset: Math.max(0, anchorRow - transcriptRowAt(prefixRows, idx)) };
11674
11955
  transcriptAnchorDirtyRef.current = false;
11675
11956
  } else {
11676
11957
  transcriptAnchorDirtyRef.current = true;
@@ -11867,6 +12148,7 @@ function useTranscriptWindow({
11867
12148
  const transcriptTotalRowsRef = useRef9(0);
11868
12149
  const committedMaxScrollRowsRef = useRef9(0);
11869
12150
  const incrementalRowIndexCacheRef = useRef9(null);
12151
+ const transcriptItemsCacheRef = useRef9(null);
11870
12152
  const prevViewportGeomRef = useRef9({ contentHeight: 0, floatingPanelRows: 0 });
11871
12153
  const transcriptItemElsRef = useRef9(/* @__PURE__ */ new Map());
11872
12154
  const transcriptMeasureRefCache = useRef9(/* @__PURE__ */ new Map());
@@ -11901,12 +12183,18 @@ function useTranscriptWindow({
11901
12183
  const tailRows = streamingTailItem ? estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded) : 0;
11902
12184
  const tailSig = streamingTailItem ? `${streamingTailItem.id}:${tailRows}` : "_";
11903
12185
  const revision = Math.max(0, Number(structureRevision) || 0);
11904
- const transcriptItems = useMemo(
11905
- () => streamingTailItem ? [...settledItems || [], streamingTailItem] : settledItems || [],
11906
- // eslint-disable-next-line react-hooks/exhaustive-deps -- tail text at the same resolved height is injected into the bounded visible slice below
11907
- [settledItems, revision, streamingTailItem?.id, tailRows]
12186
+ const transcriptItems = transcriptItemsWithStableTail(
12187
+ settledItems,
12188
+ streamingTailItem,
12189
+ transcriptItemsCacheRef
12190
+ );
12191
+ const transcriptStructureSig = transcriptStructureSignature(
12192
+ transcriptItems,
12193
+ frameColumns,
12194
+ toolOutputExpanded,
12195
+ revision,
12196
+ streamingTailItem
11908
12197
  );
11909
- const transcriptStructureSig = `${revision}#${tailSig}`;
11910
12198
  const transcriptStreamingActive = !!streamingTailItem;
11911
12199
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > 0;
11912
12200
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
@@ -11919,7 +12207,8 @@ function useTranscriptWindow({
11919
12207
  suppressMeasuredRowHeights,
11920
12208
  measuredRowsVersion,
11921
12209
  cacheRef: incrementalRowIndexCacheRef,
11922
- prefixRevision: revision
12210
+ prefixRevision: revision,
12211
+ streamingTailItem
11923
12212
  // eslint-disable-next-line react-hooks/exhaustive-deps -- revision/tail height capture structural geometry; measuredRowsVersion folds in measured corrections
11924
12213
  }), [revision, tailSig, frameColumns, toolOutputExpanded, measuredRowsVersion, suppressMeasuredRowHeights]);
11925
12214
  const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
@@ -11946,7 +12235,7 @@ function useTranscriptWindow({
11946
12235
  } else if (!followingRef.current && !nearBottomWithoutAnchor && scrolledUp) {
11947
12236
  const geom = transcriptGeomRef.current || {};
11948
12237
  const prevPrefix = geom.prefixRows;
11949
- if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
12238
+ if (prevPrefix && prevPrefix.length > 1) {
11950
12239
  const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
11951
12240
  const prevView = Math.max(1, Number(geom.viewRows) || 1);
11952
12241
  const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
@@ -11957,7 +12246,7 @@ function useTranscriptWindow({
11957
12246
  if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
11958
12247
  const anchorItem = prevItems[idx];
11959
12248
  if (anchorItem && anchorItem.id != null) {
11960
- const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
12249
+ const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - transcriptRowAt(prevPrefix, idx)) };
11961
12250
  const locked = resolveAnchorScrollOffset({
11962
12251
  anchor: captured,
11963
12252
  items: transcriptItems,
@@ -11979,7 +12268,7 @@ function useTranscriptWindow({
11979
12268
  if (viewportOnlyChanged && !anchorLockActive && !followingRef.current && !nearBottomWithoutAnchor && scrolledUp) {
11980
12269
  const geom = transcriptGeomRef.current || {};
11981
12270
  const prevPrefix = geom.prefixRows;
11982
- if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
12271
+ if (prevPrefix && prevPrefix.length > 1) {
11983
12272
  const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
11984
12273
  const prevView = Math.max(1, Number(prevViewport.contentHeight) || Number(geom.viewRows) || 1);
11985
12274
  const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
@@ -11990,7 +12279,7 @@ function useTranscriptWindow({
11990
12279
  if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
11991
12280
  const anchorItem = prevItems[idx];
11992
12281
  if (anchorItem && anchorItem.id != null) {
11993
- const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
12282
+ const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - transcriptRowAt(prevPrefix, idx)) };
11994
12283
  const locked = resolveAnchorScrollOffset({
11995
12284
  anchor: captured,
11996
12285
  items: transcriptItems,
@@ -12212,7 +12501,7 @@ function useTranscriptWindow({
12212
12501
  if (idx > itemList.length - 1) idx = itemList.length - 1;
12213
12502
  const anchorItem = itemList[idx];
12214
12503
  if (anchorItem && anchorItem.id != null) {
12215
- anchor = { id: anchorItem.id, offset: Math.max(0, anchorRow - (curPrefix[idx] || 0)) };
12504
+ anchor = { id: anchorItem.id, offset: Math.max(0, anchorRow - transcriptRowAt(curPrefix, idx)) };
12216
12505
  transcriptAnchorRef.current = anchor;
12217
12506
  }
12218
12507
  }
@@ -21657,9 +21946,11 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
21657
21946
  overlayHintFallbackRow,
21658
21947
  transcriptMeasureRef
21659
21948
  } = useTranscriptWindow({
21660
- items: state.items,
21661
- structureRevision: state.structureRevision,
21662
- streamingTail: state.streamingTail,
21949
+ items: state.transcriptViewItems || state.items,
21950
+ structureRevision: state.transcriptViewItems ? state.transcriptViewRevision : state.structureRevision,
21951
+ // Historical pages are contiguous settled windows. Keep the independently
21952
+ // growing live tail hidden until paging forward reaches the live window.
21953
+ streamingTail: state.transcriptViewItems ? null : state.streamingTail,
21663
21954
  themeEpoch: state.themeEpoch,
21664
21955
  frameColumns,
21665
21956
  toolOutputExpanded,
@@ -21812,7 +22103,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
21812
22103
  Item,
21813
22104
  {
21814
22105
  item,
21815
- prevKind: i > 0 ? arr[i - 1].kind : state.items[transcriptWindow.startIndex - 1]?.kind ?? null,
22106
+ prevKind: i > 0 ? arr[i - 1].kind : (state.transcriptViewItems || state.items)[transcriptWindow.startIndex - 1]?.kind ?? null,
21816
22107
  columns: frameColumns,
21817
22108
  toolOutputExpanded,
21818
22109
  rightMessage: attachOverlayHint ? inputHint : "",
@@ -22094,6 +22385,11 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
22094
22385
 
22095
22386
  // src/tui/engine.mjs
22096
22387
  import { performance as performance2 } from "node:perf_hooks";
22388
+ import { mkdtempSync, readFileSync as readFileSync8, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
22389
+ import { randomUUID } from "node:crypto";
22390
+ import { tmpdir as tmpdir2 } from "node:os";
22391
+ import { join as join10 } from "node:path";
22392
+ import { Worker } from "node:worker_threads";
22097
22393
 
22098
22394
  // src/runtime/shared/tool-execution-contract.mjs
22099
22395
  function clean2(value) {
@@ -23027,71 +23323,6 @@ function resolveTuiRuntimeNotificationDelivery(event, text) {
23027
23323
  };
23028
23324
  }
23029
23325
 
23030
- // src/tui/engine/render-timing.mjs
23031
- var RENDER_ACK_HANG_GUARD_MS = 250;
23032
- var RENDER_SETTLE_IDLE_MS = 64;
23033
- var pendingRenderAcks = [];
23034
- var renderAckSeq = 0;
23035
- var scheduleRenderFrameAck = () => {
23036
- const seq = ++renderAckSeq;
23037
- setImmediate(() => notifyRenderFrame(seq));
23038
- };
23039
- var notifyRenderFrame = (seq = ++renderAckSeq) => {
23040
- if (pendingRenderAcks.length === 0) return;
23041
- const acks = pendingRenderAcks;
23042
- pendingRenderAcks = [];
23043
- for (const ack of acks) ack(seq);
23044
- };
23045
- var yieldToRenderer = ({ frames = 1 } = {}) => new Promise((resolve5) => {
23046
- const minSeq = renderAckSeq;
23047
- let remainingFrames = Math.max(1, Math.floor(Number(frames) || 1));
23048
- let settled = false;
23049
- let sawRealFrame = false;
23050
- let timer2 = null;
23051
- const finish = () => {
23052
- if (settled) return;
23053
- settled = true;
23054
- if (timer2) clearTimeout(timer2);
23055
- const idx = pendingRenderAcks.indexOf(onFrame);
23056
- if (idx !== -1) pendingRenderAcks.splice(idx, 1);
23057
- resolve5();
23058
- };
23059
- const onTimeout = () => {
23060
- if (settled) return;
23061
- if (!sawRealFrame && renderAckSeq > minSeq) {
23062
- setImmediate(() => {
23063
- if (!settled && !sawRealFrame) finish();
23064
- });
23065
- return;
23066
- }
23067
- finish();
23068
- };
23069
- const armWait = () => {
23070
- if (settled) return;
23071
- if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
23072
- if (timer2) clearTimeout(timer2);
23073
- timer2 = setTimeout(onTimeout, sawRealFrame ? RENDER_SETTLE_IDLE_MS : RENDER_ACK_HANG_GUARD_MS);
23074
- };
23075
- const onFrame = (seq = 0) => {
23076
- if (settled) return;
23077
- if (seq <= minSeq) {
23078
- if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
23079
- return;
23080
- }
23081
- sawRealFrame = true;
23082
- if (timer2) {
23083
- clearTimeout(timer2);
23084
- timer2 = null;
23085
- }
23086
- const idx = pendingRenderAcks.indexOf(onFrame);
23087
- if (idx !== -1) pendingRenderAcks.splice(idx, 1);
23088
- remainingFrames -= 1;
23089
- if (remainingFrames <= 0) finish();
23090
- else armWait();
23091
- };
23092
- armWait();
23093
- });
23094
-
23095
23326
  // src/tui/engine/tool-result-status.mjs
23096
23327
  var CANCELLED_RESULT_STATUS_LINE = "[status: cancelled]";
23097
23328
  function shellCommandExitCode(text) {
@@ -24151,7 +24382,7 @@ function drainTuiSteeringPersist(leadSessionId) {
24151
24382
  }
24152
24383
 
24153
24384
  // src/tui/engine/context-state.mjs
24154
- function createContextState({ runtime, getState, getPendingSessionReset }) {
24385
+ function createContextState({ runtime, getState, updateState, getPendingSessionReset }) {
24155
24386
  const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1e3, custom: false, providerDefault: 60 * 60 * 1e3, provider: null, minContextPercent: 10 };
24156
24387
  const AGENT_STATUS_CACHE_MS = 250;
24157
24388
  let agentStatusCache = null;
@@ -24201,15 +24432,16 @@ function createContextState({ runtime, getState, getPendingSessionReset }) {
24201
24432
  function syncContextDisplayFields(ctx = null) {
24202
24433
  const status = ctx || runtime.contextStatus?.() || null;
24203
24434
  if (!status) return;
24204
- const state = getState();
24205
24435
  const displayWindow = Number(status.contextWindow || 0);
24206
24436
  const compactBoundary = Number(status.compaction?.boundaryTokens || 0);
24207
24437
  const autoCompact = Number(
24208
24438
  status.compaction?.triggerTokens || status.compaction?.autoCompactTokenLimit || runtime.session?.autoCompactTokenLimit || 0
24209
24439
  );
24210
- if (displayWindow > 0) state.displayContextWindow = displayWindow;
24211
- if (compactBoundary > 0) state.compactBoundaryTokens = compactBoundary;
24212
- if (autoCompact > 0) state.autoCompactTokenLimit = autoCompact;
24440
+ const patch = {};
24441
+ if (displayWindow > 0) patch.displayContextWindow = displayWindow;
24442
+ if (compactBoundary > 0) patch.compactBoundaryTokens = compactBoundary;
24443
+ if (autoCompact > 0) patch.autoCompactTokenLimit = autoCompact;
24444
+ if (Object.keys(patch).length > 0) updateState(patch);
24213
24445
  }
24214
24446
  const syncContextStats = ({ allowEstimated = false } = {}) => {
24215
24447
  if (getPendingSessionReset()) return null;
@@ -24217,15 +24449,17 @@ function createContextState({ runtime, getState, getPendingSessionReset }) {
24217
24449
  if (!ctx) return null;
24218
24450
  syncContextDisplayFields(ctx);
24219
24451
  const state = getState();
24220
- const hasProviderUsage = Number(state.stats.latestPromptTokens || state.stats.latestInputTokens || state.stats.inputTokens || 0) > 0;
24452
+ const stats = { ...state.stats };
24453
+ const hasProviderUsage = Number(stats.latestPromptTokens || stats.latestInputTokens || stats.inputTokens || 0) > 0;
24221
24454
  const hasApiContextUsage = Number(ctx?.lastApiRequestTokens ?? ctx?.usage?.lastContextTokens ?? 0) > 0;
24222
24455
  const hasTurnActivity = state.busy === true || state.spinner != null || state.thinking != null;
24223
24456
  const isFreshSession = !hasProviderUsage && !hasApiContextUsage && !hasTurnActivity;
24224
24457
  if (isFreshSession) {
24225
- state.stats.currentEstimatedContextTokens = 0;
24226
- state.stats.currentContextTokens = 0;
24227
- state.stats.currentContextSource = null;
24228
- state.stats.currentContextUpdatedAt = Date.now();
24458
+ stats.currentEstimatedContextTokens = 0;
24459
+ stats.currentContextTokens = 0;
24460
+ stats.currentContextSource = null;
24461
+ stats.currentContextUpdatedAt = Date.now();
24462
+ updateState({ stats });
24229
24463
  return ctx;
24230
24464
  }
24231
24465
  const estimatedTokens = Math.max(0, Number(ctx.currentEstimatedTokens ?? ctx.usedTokens ?? 0));
@@ -24234,32 +24468,33 @@ function createContextState({ runtime, getState, getPendingSessionReset }) {
24234
24468
  const shouldPublishEstimate = allowEstimated && (usedSource === "estimated" || Number(ctx.currentEstimatedTokens) > 0 || usedTokens > 0);
24235
24469
  if (!allowEstimated && !hasProviderUsage && usedSource !== "last_api_request") return ctx;
24236
24470
  if (shouldPublishEstimate) {
24237
- state.stats.currentEstimatedContextTokens = estimatedTokens;
24238
- state.stats.currentContextSource = "estimated";
24239
- state.stats.currentContextTokens = 0;
24471
+ stats.currentEstimatedContextTokens = estimatedTokens;
24472
+ stats.currentContextSource = "estimated";
24473
+ stats.currentContextTokens = 0;
24240
24474
  } else if (allowEstimated && (hasProviderUsage || hasApiContextUsage || hasTurnActivity)) {
24241
- state.stats.currentEstimatedContextTokens = estimatedTokens;
24242
- state.stats.currentContextSource = usedSource || (estimatedTokens > 0 ? "estimated" : null);
24243
- const publishedSource = String(state.stats.currentContextSource || "").toLowerCase();
24475
+ stats.currentEstimatedContextTokens = estimatedTokens;
24476
+ stats.currentContextSource = usedSource || (estimatedTokens > 0 ? "estimated" : null);
24477
+ const publishedSource = String(stats.currentContextSource || "").toLowerCase();
24244
24478
  if (publishedSource === "last_api_request") {
24245
24479
  const apiUsed = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
24246
- state.stats.currentContextTokens = apiUsed;
24480
+ stats.currentContextTokens = apiUsed;
24247
24481
  } else if (publishedSource === "estimated") {
24248
- state.stats.currentContextTokens = 0;
24482
+ stats.currentContextTokens = 0;
24249
24483
  } else {
24250
- state.stats.currentContextTokens = usedTokens > 0 ? usedTokens : 0;
24484
+ stats.currentContextTokens = usedTokens > 0 ? usedTokens : 0;
24251
24485
  }
24252
24486
  } else {
24253
- state.stats.currentEstimatedContextTokens = 0;
24487
+ stats.currentEstimatedContextTokens = 0;
24254
24488
  if (usedSource === "last_api_request" && Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0) > 0) {
24255
- state.stats.currentContextTokens = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
24256
- state.stats.currentContextSource = "last_api_request";
24489
+ stats.currentContextTokens = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
24490
+ stats.currentContextSource = "last_api_request";
24257
24491
  } else {
24258
- state.stats.currentContextTokens = 0;
24259
- state.stats.currentContextSource = null;
24492
+ stats.currentContextTokens = 0;
24493
+ stats.currentContextSource = null;
24260
24494
  }
24261
24495
  }
24262
- state.stats.currentContextUpdatedAt = Date.now();
24496
+ stats.currentContextUpdatedAt = Date.now();
24497
+ updateState({ stats });
24263
24498
  return ctx;
24264
24499
  };
24265
24500
  return {
@@ -24305,6 +24540,7 @@ function createSessionFlow(bag) {
24305
24540
  clearToastTimers,
24306
24541
  getState,
24307
24542
  set,
24543
+ flushEmitImmediate,
24308
24544
  pushItem,
24309
24545
  replaceItems,
24310
24546
  pushNotice,
@@ -24313,7 +24549,10 @@ function createSessionFlow(bag) {
24313
24549
  agentStatusState,
24314
24550
  routeState,
24315
24551
  syncContextStats,
24316
- flushDeferredExecutionPendingResumeKick
24552
+ flushDeferredExecutionPendingResumeKick,
24553
+ snapshotTranscriptSpill,
24554
+ restoreTranscriptSpill,
24555
+ releaseTranscriptSpill
24317
24556
  } = bag;
24318
24557
  const AUTO_CLEAR_COMPACT_TIMEOUT_MS = 6e4;
24319
24558
  const leadSessionId = () => runtime.id;
@@ -24516,7 +24755,10 @@ function createSessionFlow(bag) {
24516
24755
  if (getState().busy && shouldMirrorSteeringEntry(entry)) {
24517
24756
  appendTuiSteeringPersist(leadSessionId(), entry);
24518
24757
  }
24519
- if (isQueuedEntryVisible(entry)) set({ queued: [...getState().queued, entry] });
24758
+ if (isQueuedEntryVisible(entry)) {
24759
+ set({ queued: [...getState().queued, entry] });
24760
+ if (isQueuedEntryEditable(entry)) flushEmitImmediate?.();
24761
+ }
24520
24762
  if (getState().busy) tuiDebug2(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
24521
24763
  void drain();
24522
24764
  return true;
@@ -24698,19 +24940,23 @@ function createSessionFlow(bag) {
24698
24940
  return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued), pastedTexts: mergePastedTexts(queued) };
24699
24941
  }
24700
24942
  const resetStats = () => {
24701
- getState().stats = createSessionStats();
24702
- return getState().stats;
24943
+ const stats = createSessionStats();
24944
+ set({ stats });
24945
+ return stats;
24703
24946
  };
24704
24947
  const clearUiActivityBeforeContextSync = () => {
24705
24948
  clearToastTimers();
24706
24949
  resetAllStreamingMarkdownStablePrefixes();
24707
- getState().items = replaceItems([]);
24708
- getState().toasts = [];
24709
- getState().queued = [];
24710
- getState().thinking = null;
24711
- getState().spinner = null;
24712
- getState().lastTurn = null;
24713
- getState().busy = false;
24950
+ const items = replaceItems([]);
24951
+ set({
24952
+ items,
24953
+ toasts: [],
24954
+ queued: [],
24955
+ thinking: null,
24956
+ spinner: null,
24957
+ lastTurn: null,
24958
+ busy: false
24959
+ });
24714
24960
  pendingNotificationKeys.clear();
24715
24961
  displayedExecutionNotificationKeys.clear();
24716
24962
  clearExecutionDedupState?.();
@@ -24743,16 +24989,24 @@ function createSessionFlow(bag) {
24743
24989
  flags.pendingSessionReset = true;
24744
24990
  clearUiActivityBeforeContextSync();
24745
24991
  resetStats();
24746
- getState().stats.currentContextTokens = 0;
24747
- getState().stats.currentEstimatedContextTokens = 0;
24748
- getState().stats.currentContextSource = null;
24749
- getState().stats.currentContextUpdatedAt = Date.now();
24750
- getState().displayContextWindow = 0;
24751
- getState().compactBoundaryTokens = 0;
24752
- getState().autoCompactTokenLimit = 0;
24992
+ set({
24993
+ stats: {
24994
+ ...getState().stats,
24995
+ currentContextTokens: 0,
24996
+ currentEstimatedContextTokens: 0,
24997
+ currentContextSource: null,
24998
+ currentContextUpdatedAt: Date.now()
24999
+ },
25000
+ displayContextWindow: 0,
25001
+ compactBoundaryTokens: 0,
25002
+ autoCompactTokenLimit: 0
25003
+ });
24753
25004
  };
24754
25005
  const snapshotTuiBeforeSessionReset = () => ({
24755
25006
  items: getState().items.slice(),
25007
+ transcriptViewItems: Array.isArray(getState().transcriptViewItems) ? getState().transcriptViewItems.slice() : null,
25008
+ transcriptViewRevision: getState().transcriptViewRevision,
25009
+ transcriptSpill: snapshotTranscriptSpill?.() || null,
24756
25010
  toasts: getState().toasts.slice(),
24757
25011
  queued: getState().queued.slice(),
24758
25012
  thinking: getState().thinking,
@@ -24765,14 +25019,20 @@ function createSessionFlow(bag) {
24765
25019
  const restoreTuiAfterFailedSessionReset = (snapshot) => {
24766
25020
  if (!snapshot) return;
24767
25021
  flags.pendingSessionReset = false;
24768
- getState().items = replaceItems(snapshot.items);
24769
- getState().toasts = snapshot.toasts.slice();
24770
- getState().queued = snapshot.queued.slice();
24771
- getState().thinking = snapshot.thinking;
24772
- getState().spinner = snapshot.spinner;
24773
- getState().lastTurn = snapshot.lastTurn;
24774
- getState().busy = snapshot.busy;
24775
- getState().stats = { ...snapshot.stats };
25022
+ restoreTranscriptSpill?.(snapshot.transcriptSpill);
25023
+ const items = replaceItems(snapshot.items, { preserveSpill: true });
25024
+ set({
25025
+ items,
25026
+ transcriptViewItems: snapshot.transcriptViewItems,
25027
+ transcriptViewRevision: snapshot.transcriptViewRevision,
25028
+ toasts: snapshot.toasts.slice(),
25029
+ queued: snapshot.queued.slice(),
25030
+ thinking: snapshot.thinking,
25031
+ spinner: snapshot.spinner,
25032
+ lastTurn: snapshot.lastTurn,
25033
+ busy: snapshot.busy,
25034
+ stats: { ...snapshot.stats }
25035
+ });
24776
25036
  syncContextStats({ allowEstimated: true });
24777
25037
  set({
24778
25038
  items: getState().items,
@@ -24787,12 +25047,15 @@ function createSessionFlow(bag) {
24787
25047
  ...agentStatusState()
24788
25048
  });
24789
25049
  };
25050
+ const commitTuiSessionReset = (snapshot) => {
25051
+ releaseTranscriptSpill?.(snapshot?.transcriptSpill);
25052
+ };
24790
25053
  const resetStatsAndSyncContext = () => {
24791
25054
  resetStats();
24792
25055
  syncContextStats({ allowEstimated: true });
24793
25056
  return getState().stats;
24794
25057
  };
24795
- return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, resetStatsAndSyncContext };
25058
+ return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext };
24796
25059
  }
24797
25060
 
24798
25061
  // src/tui/engine/turn.mjs
@@ -24807,7 +25070,10 @@ function createRunTurn(bag) {
24807
25070
  itemIndexById,
24808
25071
  getState,
24809
25072
  set,
25073
+ flushEmit,
25074
+ flushEmitImmediate,
24810
25075
  pushItem,
25076
+ appendItems,
24811
25077
  patchItem,
24812
25078
  replaceItems,
24813
25079
  updateStreamingTail: updateStreamingTailFromStore,
@@ -24976,6 +25242,7 @@ function createRunTurn(bag) {
24976
25242
  if (flags.draining) flags.draining = false;
24977
25243
  if (pending.length > 0) void drain();
24978
25244
  flushDeferredExecutionPendingResumeKick();
25245
+ flushEmitImmediate?.();
24979
25246
  }, 5e3);
24980
25247
  watchdogGraceTimer.unref?.();
24981
25248
  }, delay);
@@ -25128,6 +25395,10 @@ function createRunTurn(bag) {
25128
25395
  set(extra);
25129
25396
  return;
25130
25397
  }
25398
+ if (appendItems) {
25399
+ appendItems(newItems, extra);
25400
+ return;
25401
+ }
25131
25402
  const base = getState().items.length;
25132
25403
  const items = [...getState().items, ...newItems];
25133
25404
  for (let i = 0; i < newItems.length; i++) {
@@ -25637,6 +25908,37 @@ function createRunTurn(bag) {
25637
25908
  if (thinkingLastEndedAt) _pendingThinkingLastEndedAt = thinkingLastEndedAt;
25638
25909
  scheduleStreamFlush();
25639
25910
  },
25911
+ onTextReset: ({ chars } = {}) => {
25912
+ if (!isCurrentTurn()) return false;
25913
+ const count = Math.max(0, Number(chars) || 0);
25914
+ if (!count) return false;
25915
+ flushStreamBatch();
25916
+ assistantText = assistantText.slice(0, Math.max(0, assistantText.length - count));
25917
+ currentAssistantText = currentAssistantText.slice(
25918
+ 0,
25919
+ Math.max(0, currentAssistantText.length - count)
25920
+ );
25921
+ _streamScanLen = 0;
25922
+ _lastNewlineIdx = -1;
25923
+ _emittedNewlineIdx = -2;
25924
+ _emittedVisibleText = "";
25925
+ if (currentAssistantId) {
25926
+ if (currentAssistantText) {
25927
+ set({
25928
+ streamingTail: {
25929
+ kind: "assistant",
25930
+ id: currentAssistantId,
25931
+ text: currentAssistantText,
25932
+ streaming: true
25933
+ }
25934
+ });
25935
+ } else {
25936
+ clearStreamingTail(currentAssistantId);
25937
+ currentAssistantId = null;
25938
+ }
25939
+ }
25940
+ return true;
25941
+ },
25640
25942
  onAssistantText: (text) => {
25641
25943
  const full = String(text ?? "");
25642
25944
  if (!full.trim()) return;
@@ -25667,7 +25969,9 @@ function createRunTurn(bag) {
25667
25969
  },
25668
25970
  onUsageDelta: (delta) => {
25669
25971
  if (!markTurnProgress("usage-delta")) return;
25670
- applyUsageDelta(getState().stats, delta);
25972
+ const stats = { ...getState().stats };
25973
+ applyUsageDelta(stats, delta);
25974
+ set({ stats });
25671
25975
  syncContextStats({ allowEstimated: true });
25672
25976
  const currentTurnInput = Math.max(0, getState().stats.inputTokens - inputBaseline);
25673
25977
  const currentTurnOutput = Math.max(0, getState().stats.outputTokens - outputBaseline);
@@ -25770,7 +26074,7 @@ function createRunTurn(bag) {
25770
26074
  const assistantOutput = (currentAssistantText || assistantText || "").trim();
25771
26075
  const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
25772
26076
  if (!isNoOpTurn) {
25773
- getState().stats.turns = (getState().stats.turns || 0) + 1;
26077
+ set({ stats: { ...getState().stats, turns: (getState().stats.turns || 0) + 1 } });
25774
26078
  }
25775
26079
  if (!reclaimed && !isNoOpTurn) {
25776
26080
  closingItems.push({ kind: "turndone", id: nextId2(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
@@ -25789,6 +26093,7 @@ function createRunTurn(bag) {
25789
26093
  }
25790
26094
  }
25791
26095
  if (flags.leadTurnEpoch === turnEpoch) clearActiveToolSummary();
26096
+ flushEmit?.();
25792
26097
  _publishedThinkingActive = false;
25793
26098
  const finalStatus = cancelled ? "cancelled" : failed ? "failed" : "done";
25794
26099
  tuiDebug2(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt}${watchdogTripped ? " watchdogTripped=1" : ""} pending=${pending.length}`);
@@ -25807,11 +26112,13 @@ function createEngineApiB(bag) {
25807
26112
  listeners,
25808
26113
  getState,
25809
26114
  set,
26115
+ disposeEmit,
25810
26116
  replaceItems,
25811
26117
  pushNotice,
25812
26118
  removeNotice,
25813
26119
  setProgressHint,
25814
26120
  clearToastTimers,
26121
+ disposeTranscriptSpill,
25815
26122
  routeState,
25816
26123
  syncContextStats,
25817
26124
  finishToolApproval,
@@ -25822,6 +26129,7 @@ function createEngineApiB(bag) {
25822
26129
  resetTuiForPendingSessionReset,
25823
26130
  snapshotTuiBeforeSessionReset,
25824
26131
  restoreTuiAfterFailedSessionReset,
26132
+ commitTuiSessionReset,
25825
26133
  resetStatsAndSyncContext
25826
26134
  } = bag;
25827
26135
  return {
@@ -26166,6 +26474,7 @@ function createEngineApiB(bag) {
26166
26474
  flags.pendingSessionReset = false;
26167
26475
  resetStatsAndSyncContext();
26168
26476
  set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...getState().stats } });
26477
+ commitTuiSessionReset(rollbackSnapshot);
26169
26478
  flags.lastUserActivityAt = Date.now();
26170
26479
  return true;
26171
26480
  } catch (error) {
@@ -26203,6 +26512,7 @@ function createEngineApiB(bag) {
26203
26512
  ...routeState(),
26204
26513
  stats: { ...getState().stats }
26205
26514
  });
26515
+ commitTuiSessionReset(rollbackSnapshot);
26206
26516
  return true;
26207
26517
  } catch (error) {
26208
26518
  restoreTuiAfterFailedSessionReset(rollbackSnapshot);
@@ -26235,6 +26545,7 @@ function createEngineApiB(bag) {
26235
26545
  flags.pendingSessionReset = false;
26236
26546
  resetStatsAndSyncContext();
26237
26547
  set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...getState().stats } });
26548
+ commitTuiSessionReset(rollbackSnapshot);
26238
26549
  return true;
26239
26550
  } catch (error) {
26240
26551
  restoreTuiAfterFailedSessionReset(rollbackSnapshot);
@@ -26305,8 +26616,10 @@ function createEngineApiB(bag) {
26305
26616
  },
26306
26617
  dispose: async (reason = "cli-react-exit", options = {}) => {
26307
26618
  if (flags.disposed) return;
26619
+ disposeEmit?.();
26308
26620
  flags.disposed = true;
26309
26621
  clearToastTimers();
26622
+ disposeTranscriptSpill?.();
26310
26623
  try {
26311
26624
  clearInterval(lifecycle.runtimePulseTimer);
26312
26625
  } catch {
@@ -26531,10 +26844,14 @@ function createEngineApiA(bag) {
26531
26844
  pending,
26532
26845
  listeners,
26533
26846
  getState,
26847
+ getPublishedState = getState,
26534
26848
  set,
26849
+ flushEmitImmediate,
26535
26850
  pushItem,
26536
26851
  patchItem,
26537
26852
  replaceItems,
26853
+ restoreOlderTranscript,
26854
+ restoreNewerTranscript,
26538
26855
  settleStreamingTail,
26539
26856
  clearStreamingTail,
26540
26857
  pushNotice,
@@ -26554,8 +26871,10 @@ function createEngineApiA(bag) {
26554
26871
  discardExecutionPendingResume
26555
26872
  } = bag;
26556
26873
  return {
26557
- getState: () => getState(),
26874
+ getState: () => getPublishedState(),
26558
26875
  patchItem,
26876
+ restoreOlderTranscript,
26877
+ restoreNewerTranscript,
26559
26878
  subscribe: (listener) => {
26560
26879
  listeners.add(listener);
26561
26880
  return () => listeners.delete(listener);
@@ -27063,7 +27382,10 @@ function createEngineApiA(bag) {
27063
27382
  if (idSet.size > 0) {
27064
27383
  const items = getState().items.filter((item) => !idSet.has(item?.id));
27065
27384
  if (items.length !== getState().items.length) {
27066
- patch.items = replaceItems(items);
27385
+ patch.items = replaceItems(items, {
27386
+ preserveSpill: true,
27387
+ preserveTranscriptView: true
27388
+ });
27067
27389
  }
27068
27390
  }
27069
27391
  set(patch);
@@ -27091,6 +27413,7 @@ function createEngineApiA(bag) {
27091
27413
  flags.drainEpoch = (Number(flags.drainEpoch) || 0) + 1;
27092
27414
  if (flags.draining) flags.draining = false;
27093
27415
  pushNotice("Interrupt did not settle \u2014 input restored.", "warn", { transcript: true });
27416
+ flushEmitImmediate?.();
27094
27417
  if (pending.length > 0 && typeof drain === "function") void drain();
27095
27418
  if (typeof flushDeferredExecutionPendingResumeKick === "function") flushDeferredExecutionPendingResumeKick();
27096
27419
  }, recoveryMs);
@@ -27100,6 +27423,68 @@ function createEngineApiA(bag) {
27100
27423
  };
27101
27424
  }
27102
27425
 
27426
+ // src/tui/engine/frame-batched-store.mjs
27427
+ function createFrameBatchedStorePublisher({
27428
+ getState,
27429
+ publishState,
27430
+ listeners,
27431
+ isDisposed = () => false,
27432
+ frameMs = 16,
27433
+ setTimer = setTimeout,
27434
+ clearTimer = clearTimeout,
27435
+ enqueueMicrotask = queueMicrotask
27436
+ }) {
27437
+ let timer2 = null;
27438
+ let emitPending = false;
27439
+ let structureChangePending = false;
27440
+ let immediatePending = false;
27441
+ const flush = () => {
27442
+ if (timer2 !== null) {
27443
+ clearTimer(timer2);
27444
+ timer2 = null;
27445
+ }
27446
+ immediatePending = false;
27447
+ if (!emitPending || isDisposed()) return false;
27448
+ emitPending = false;
27449
+ const current = getState();
27450
+ let next = current;
27451
+ if (structureChangePending) {
27452
+ structureChangePending = false;
27453
+ next = {
27454
+ ...current,
27455
+ structureRevision: (Number(current.structureRevision) || 0) + 1
27456
+ };
27457
+ }
27458
+ publishState(next);
27459
+ for (const listener of listeners) listener();
27460
+ return true;
27461
+ };
27462
+ const emit = () => {
27463
+ emitPending = true;
27464
+ if (timer2 !== null || isDisposed()) return;
27465
+ timer2 = setTimer(flush, frameMs);
27466
+ timer2?.unref?.();
27467
+ };
27468
+ const markStructureChange = () => {
27469
+ structureChangePending = true;
27470
+ };
27471
+ const flushImmediate = () => {
27472
+ if (!emitPending || immediatePending || isDisposed()) return false;
27473
+ immediatePending = true;
27474
+ enqueueMicrotask(flush);
27475
+ return true;
27476
+ };
27477
+ const dispose = () => {
27478
+ if (emitPending && !isDisposed()) flush();
27479
+ else if (timer2 !== null) clearTimer(timer2);
27480
+ timer2 = null;
27481
+ emitPending = false;
27482
+ structureChangePending = false;
27483
+ immediatePending = false;
27484
+ };
27485
+ return { emit, flush, flushImmediate, markStructureChange, dispose };
27486
+ }
27487
+
27103
27488
  // src/tui/engine.mjs
27104
27489
  var SESSION_RUNTIME_MODULE = import.meta.url.replace(/\\/g, "/").includes("/tui/dist/") ? "../../mixdog-session-runtime.mjs" : "../mixdog-session-runtime.mjs";
27105
27490
  var TOOL_APPROVAL_TIMEOUT_MS = (() => {
@@ -27121,6 +27506,377 @@ var tuiDebug = (msg) => {
27121
27506
  };
27122
27507
  var _idSeq = 0;
27123
27508
  var nextId = () => `it_${++_idSeq}`;
27509
+ var TRANSCRIPT_LIVE_ITEM_CAP = 512;
27510
+ var TRANSCRIPT_SPILL_CHUNK_ITEMS = 128;
27511
+ var TRANSCRIPT_RESTORE_OVERLAP_ITEMS = 64;
27512
+ var TRANSCRIPT_SPILL_STALE_MS = 24 * 60 * 60 * 1e3;
27513
+ var TRANSCRIPT_SPILL_HEARTBEAT_MS = 1e4;
27514
+ var TRANSCRIPT_PROCESS_NONCE = randomUUID();
27515
+ function cleanupStaleTranscriptSpillDirs({
27516
+ root = tmpdir2(),
27517
+ now = Date.now(),
27518
+ staleMs = TRANSCRIPT_SPILL_STALE_MS
27519
+ } = {}) {
27520
+ try {
27521
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
27522
+ if (!entry.isDirectory() || !entry.name.startsWith("mixdog-transcript-")) continue;
27523
+ const path2 = join10(root, entry.name);
27524
+ try {
27525
+ const ownerPid = Number(/^mixdog-transcript-(\d+)-/.exec(entry.name)?.[1]);
27526
+ let pidAlive = false;
27527
+ if (ownerPid > 0) {
27528
+ try {
27529
+ process.kill(ownerPid, 0);
27530
+ pidAlive = true;
27531
+ } catch {
27532
+ }
27533
+ }
27534
+ if (!pidAlive) {
27535
+ rmSync2(path2, { recursive: true, force: true });
27536
+ continue;
27537
+ }
27538
+ let heartbeatAge;
27539
+ try {
27540
+ heartbeatAge = now - statSync5(join10(path2, "heartbeat")).mtimeMs;
27541
+ } catch {
27542
+ heartbeatAge = now - statSync5(path2).mtimeMs;
27543
+ }
27544
+ if (heartbeatAge <= staleMs) continue;
27545
+ rmSync2(path2, { recursive: true, force: true });
27546
+ } catch {
27547
+ }
27548
+ }
27549
+ } catch {
27550
+ }
27551
+ }
27552
+ function createTranscriptSpillBuffer({
27553
+ cap = TRANSCRIPT_LIVE_ITEM_CAP,
27554
+ chunkSize = TRANSCRIPT_SPILL_CHUNK_ITEMS,
27555
+ workerFactory = (source) => new Worker(source, { eval: true }),
27556
+ onWarning = (message) => tuiDebug(message),
27557
+ writeTimeoutMs = 5e3
27558
+ } = {}) {
27559
+ try {
27560
+ writeFileSync6(
27561
+ join10(tmpdir2(), `mixdog-transcript-owner-${process.pid}.json`),
27562
+ JSON.stringify({ pid: process.pid, nonce: TRANSCRIPT_PROCESS_NONCE }),
27563
+ "utf8"
27564
+ );
27565
+ } catch {
27566
+ }
27567
+ cleanupStaleTranscriptSpillDirs();
27568
+ const pages = [];
27569
+ let cursor = null;
27570
+ let spillDir = null;
27571
+ let pageSequence = 0;
27572
+ let spillWorker = null;
27573
+ let workerSpawnCount = 0;
27574
+ let activeWrite = null;
27575
+ let activeWriteTimer = null;
27576
+ let warningEmitted = false;
27577
+ let spillDisabled = false;
27578
+ const writeQueue = [];
27579
+ const heartbeatTimers = /* @__PURE__ */ new Map();
27580
+ const snapshots = /* @__PURE__ */ new Set();
27581
+ const cleanupRecords = (records, directory) => {
27582
+ for (const record of records) {
27583
+ record.cancelled = true;
27584
+ }
27585
+ if (directory) {
27586
+ const timer2 = heartbeatTimers.get(directory);
27587
+ if (timer2) clearInterval(timer2);
27588
+ heartbeatTimers.delete(directory);
27589
+ try {
27590
+ rmSync2(directory, { recursive: true, force: true });
27591
+ } catch {
27592
+ }
27593
+ }
27594
+ };
27595
+ const ensureSpillDir = () => {
27596
+ if (spillDir) return spillDir;
27597
+ const root = tmpdir2();
27598
+ writeFileSync6(
27599
+ join10(root, `mixdog-transcript-owner-${process.pid}.json`),
27600
+ JSON.stringify({ pid: process.pid, nonce: TRANSCRIPT_PROCESS_NONCE }),
27601
+ "utf8"
27602
+ );
27603
+ spillDir = mkdtempSync(join10(root, `mixdog-transcript-${process.pid}-${TRANSCRIPT_PROCESS_NONCE}-`));
27604
+ writeFileSync6(
27605
+ join10(spillDir, "owner.json"),
27606
+ JSON.stringify({ pid: process.pid, nonce: TRANSCRIPT_PROCESS_NONCE }),
27607
+ "utf8"
27608
+ );
27609
+ const heartbeat = join10(spillDir, "heartbeat");
27610
+ writeFileSync6(heartbeat, String(Date.now()), "utf8");
27611
+ const heartbeatTimer = setInterval(() => {
27612
+ try {
27613
+ writeFileSync6(heartbeat, String(Date.now()), "utf8");
27614
+ } catch {
27615
+ }
27616
+ }, TRANSCRIPT_SPILL_HEARTBEAT_MS);
27617
+ heartbeatTimer.unref?.();
27618
+ heartbeatTimers.set(spillDir, heartbeatTimer);
27619
+ return spillDir;
27620
+ };
27621
+ const workerSource = `
27622
+ const { parentPort } = require('node:worker_threads');
27623
+ const { renameSync, writeFileSync } = require('node:fs');
27624
+ parentPort.on('message', ({ id, targetPath, tempPath, items }) => {
27625
+ try {
27626
+ writeFileSync(tempPath, JSON.stringify(items), 'utf8');
27627
+ renameSync(tempPath, targetPath);
27628
+ parentPort.postMessage({ id, ok: true });
27629
+ } catch (error) {
27630
+ parentPort.postMessage({ id, ok: false, error: String(error && error.message || error) });
27631
+ }
27632
+ });`;
27633
+ const ensureWorker = () => {
27634
+ if (spillWorker) return spillWorker;
27635
+ try {
27636
+ const worker = workerFactory(workerSource);
27637
+ spillWorker = worker;
27638
+ workerSpawnCount += 1;
27639
+ worker.on("message", (result) => {
27640
+ if (spillWorker !== worker || result?.id !== activeWrite?.id) return;
27641
+ finishWrite(result?.ok === true, result?.error);
27642
+ });
27643
+ const failWorker = (error) => {
27644
+ if (spillWorker !== worker) return;
27645
+ if (activeWriteTimer) clearTimeout(activeWriteTimer);
27646
+ activeWriteTimer = null;
27647
+ const failed = activeWrite;
27648
+ activeWrite = null;
27649
+ spillWorker = null;
27650
+ try {
27651
+ worker.terminate?.();
27652
+ } catch {
27653
+ }
27654
+ if (failed) retryOrPin(failed, error?.message);
27655
+ pumpWrites();
27656
+ };
27657
+ worker.on("error", failWorker);
27658
+ worker.on("exit", (code) => {
27659
+ failWorker(new Error(`spill worker exited (${code})`));
27660
+ });
27661
+ worker.unref?.();
27662
+ } catch (error) {
27663
+ spillWorker = null;
27664
+ if (activeWrite) {
27665
+ const failed = activeWrite;
27666
+ activeWrite = null;
27667
+ retryOrPin(failed, error?.message);
27668
+ }
27669
+ }
27670
+ return spillWorker;
27671
+ };
27672
+ const retryOrPin = (record, error) => {
27673
+ if (record.cancelled) return;
27674
+ record.attempts += 1;
27675
+ if (record.attempts <= 2) {
27676
+ writeQueue.unshift(record);
27677
+ return;
27678
+ }
27679
+ record.pinned = true;
27680
+ spillDisabled = true;
27681
+ for (const queued of writeQueue.splice(0)) {
27682
+ if (!queued.cancelled) queued.pinned = true;
27683
+ }
27684
+ if (!warningEmitted) {
27685
+ warningEmitted = true;
27686
+ try {
27687
+ onWarning(`transcript spill write failed; history pinned in memory (${error || "unknown error"})`);
27688
+ } catch {
27689
+ }
27690
+ }
27691
+ };
27692
+ const finishWrite = (ok, error) => {
27693
+ if (activeWriteTimer) clearTimeout(activeWriteTimer);
27694
+ activeWriteTimer = null;
27695
+ const record = activeWrite;
27696
+ activeWrite = null;
27697
+ if (record && !record.cancelled) {
27698
+ if (ok) record.pendingItems = null;
27699
+ else retryOrPin(record, error);
27700
+ }
27701
+ pumpWrites();
27702
+ };
27703
+ const pumpWrites = () => {
27704
+ if (activeWrite) return;
27705
+ while (writeQueue.length && writeQueue[0].cancelled) writeQueue.shift();
27706
+ if (!writeQueue.length) return;
27707
+ activeWrite = writeQueue.shift();
27708
+ const worker = ensureWorker();
27709
+ if (!worker) {
27710
+ if (activeWrite) {
27711
+ const failed = activeWrite;
27712
+ activeWrite = null;
27713
+ retryOrPin(failed, "worker unavailable");
27714
+ }
27715
+ queueMicrotask(pumpWrites);
27716
+ return;
27717
+ }
27718
+ const tempPath = `${activeWrite.path}.attempt-${activeWrite.attempts}-${randomUUID()}.tmp`;
27719
+ worker.postMessage({
27720
+ id: activeWrite.id,
27721
+ targetPath: activeWrite.path,
27722
+ tempPath,
27723
+ items: activeWrite.pendingItems
27724
+ });
27725
+ activeWriteTimer = setTimeout(() => {
27726
+ if (!activeWrite || spillWorker !== worker) return;
27727
+ const failed = activeWrite;
27728
+ activeWrite = null;
27729
+ activeWriteTimer = null;
27730
+ spillWorker = null;
27731
+ try {
27732
+ worker.terminate?.();
27733
+ } catch {
27734
+ }
27735
+ retryOrPin(failed, `write timed out after ${writeTimeoutMs}ms`);
27736
+ pumpWrites();
27737
+ }, Math.max(1, Number(writeTimeoutMs) || 5e3));
27738
+ activeWriteTimer.unref?.();
27739
+ };
27740
+ const encode = (items) => {
27741
+ const page = join10(ensureSpillDir(), `${++pageSequence}.json`);
27742
+ const record = {
27743
+ id: pageSequence,
27744
+ path: page,
27745
+ pendingItems: items,
27746
+ cancelled: false,
27747
+ attempts: 0,
27748
+ pinned: false
27749
+ };
27750
+ writeQueue.push(record);
27751
+ pumpWrites();
27752
+ return record;
27753
+ };
27754
+ const decode = (record) => record.pendingItems || JSON.parse(readFileSync8(record.path, "utf8"));
27755
+ return {
27756
+ get hasOlder() {
27757
+ return cursor == null ? pages.length > 0 : cursor > 0;
27758
+ },
27759
+ get hasNewer() {
27760
+ return cursor != null;
27761
+ },
27762
+ reset() {
27763
+ const retained = [...snapshots].some((snapshot) => snapshot.spillDir === spillDir);
27764
+ const oldPages = pages.splice(0);
27765
+ const oldDir = spillDir;
27766
+ pages.length = 0;
27767
+ cursor = null;
27768
+ spillDir = null;
27769
+ pageSequence = 0;
27770
+ spillDisabled = false;
27771
+ warningEmitted = false;
27772
+ if (!retained) cleanupRecords(oldPages, oldDir);
27773
+ },
27774
+ snapshot() {
27775
+ const snapshot = {
27776
+ pages: pages.slice(),
27777
+ cursor,
27778
+ spillDir,
27779
+ pageSequence,
27780
+ spillDisabled,
27781
+ warningEmitted
27782
+ };
27783
+ snapshots.add(snapshot);
27784
+ return snapshot;
27785
+ },
27786
+ restoreSnapshot(snapshot) {
27787
+ if (!snapshot || !snapshots.has(snapshot)) return false;
27788
+ if (snapshot.spillDir === spillDir) {
27789
+ cursor = snapshot.cursor;
27790
+ snapshots.delete(snapshot);
27791
+ return true;
27792
+ }
27793
+ cleanupRecords(pages, spillDir);
27794
+ pages.splice(0, pages.length, ...snapshot.pages);
27795
+ cursor = snapshot.cursor;
27796
+ spillDir = snapshot.spillDir;
27797
+ pageSequence = snapshot.pageSequence;
27798
+ spillDisabled = snapshot.spillDisabled === true;
27799
+ warningEmitted = snapshot.warningEmitted === true;
27800
+ snapshots.delete(snapshot);
27801
+ return true;
27802
+ },
27803
+ releaseSnapshot(snapshot) {
27804
+ if (!snapshot || !snapshots.delete(snapshot)) return false;
27805
+ if (snapshot.spillDir !== spillDir) cleanupRecords(snapshot.pages, snapshot.spillDir);
27806
+ return true;
27807
+ },
27808
+ dispose() {
27809
+ cleanupRecords(pages, spillDir);
27810
+ for (const snapshot of snapshots) {
27811
+ if (snapshot.spillDir !== spillDir) cleanupRecords(snapshot.pages, snapshot.spillDir);
27812
+ }
27813
+ pages.length = 0;
27814
+ snapshots.clear();
27815
+ cursor = null;
27816
+ spillDir = null;
27817
+ for (const timer2 of heartbeatTimers.values()) clearInterval(timer2);
27818
+ heartbeatTimers.clear();
27819
+ writeQueue.length = 0;
27820
+ activeWrite = null;
27821
+ if (activeWriteTimer) clearTimeout(activeWriteTimer);
27822
+ activeWriteTimer = null;
27823
+ try {
27824
+ spillWorker?.terminate();
27825
+ } catch {
27826
+ }
27827
+ spillWorker = null;
27828
+ },
27829
+ get workerCount() {
27830
+ return workerSpawnCount;
27831
+ },
27832
+ get pendingWriteCount() {
27833
+ return writeQueue.length + (activeWrite ? 1 : 0);
27834
+ },
27835
+ get pinnedPageCount() {
27836
+ return pages.filter((page) => page.pinned).length;
27837
+ },
27838
+ get disabled() {
27839
+ return spillDisabled;
27840
+ },
27841
+ capLive(items) {
27842
+ let live = Array.isArray(items) ? items : [];
27843
+ if (spillDisabled) return live;
27844
+ while (live.length > cap) {
27845
+ pages.push(encode(live.slice(0, chunkSize)));
27846
+ live = live.slice(chunkSize);
27847
+ }
27848
+ return live;
27849
+ },
27850
+ restoreOlder(liveItems) {
27851
+ const nextCursor = cursor == null ? pages.length - 1 : cursor - 1;
27852
+ if (nextCursor < 0) return null;
27853
+ cursor = nextCursor;
27854
+ const restored = decode(pages[cursor]);
27855
+ const following = cursor + 1 < pages.length ? decode(pages[cursor + 1]) : Array.isArray(liveItems) ? liveItems : [];
27856
+ return [...restored, ...following.slice(0, TRANSCRIPT_RESTORE_OVERLAP_ITEMS)];
27857
+ },
27858
+ restoreNewer(liveItems) {
27859
+ if (cursor == null) return null;
27860
+ const nextCursor = cursor + 1;
27861
+ if (nextCursor >= pages.length) {
27862
+ cursor = null;
27863
+ return { items: null, atLive: true };
27864
+ }
27865
+ cursor = nextCursor;
27866
+ const restored = decode(pages[cursor]);
27867
+ const following = cursor + 1 < pages.length ? decode(pages[cursor + 1]) : Array.isArray(liveItems) ? liveItems : [];
27868
+ return [...restored, ...following.slice(0, TRANSCRIPT_RESTORE_OVERLAP_ITEMS)];
27869
+ }
27870
+ };
27871
+ }
27872
+ function refillTranscriptViewOverlap(viewItems, previousLiveItems, nextLiveItems) {
27873
+ const view = Array.isArray(viewItems) ? viewItems : null;
27874
+ if (!view) return null;
27875
+ const previousIds = new Set((previousLiveItems || []).map((item) => item?.id).filter((id) => id != null));
27876
+ if (!view.some((item) => previousIds.has(item?.id))) return view;
27877
+ const historical = view.filter((item) => !previousIds.has(item?.id));
27878
+ return [...historical, ...(nextLiveItems || []).slice(0, TRANSCRIPT_RESTORE_OVERLAP_ITEMS)];
27879
+ }
27124
27880
  function replaceEngineItemsState({
27125
27881
  state,
27126
27882
  items,
@@ -27142,7 +27898,13 @@ function replaceEngineItemsState({
27142
27898
  streamingTail: preserveStreamingTail ? state.streamingTail : null
27143
27899
  };
27144
27900
  }
27145
- function createEngineItemMutators({ getState, set, itemIndexById }) {
27901
+ function createEngineItemMutators({
27902
+ getState,
27903
+ set,
27904
+ itemIndexById,
27905
+ normalizeItems = (items) => items,
27906
+ itemStateExtra = () => ({})
27907
+ }) {
27146
27908
  const patchItem = (id, patch) => {
27147
27909
  const state = getState();
27148
27910
  let index = itemIndexById.get(id);
@@ -27182,15 +27944,21 @@ function createEngineItemMutators({ getState, set, itemIndexById }) {
27182
27944
  streaming: false
27183
27945
  };
27184
27946
  const index = state.items.length;
27185
- const items = [...state.items, item];
27186
- itemIndexById.set(id, index);
27947
+ const items = normalizeItems([...state.items, item]);
27948
+ itemIndexById.clear();
27949
+ for (let i = 0; i < items.length; i++) {
27950
+ const itemId = items[i]?.id;
27951
+ if (itemId != null) itemIndexById.set(itemId, i);
27952
+ }
27953
+ const settledIndex = items.findIndex((entry) => entry?.id === id);
27187
27954
  set({
27188
27955
  items,
27189
27956
  structureRevision: (Number(state.structureRevision) || 0) + 1,
27190
27957
  streamingTail: null,
27958
+ ...itemStateExtra(),
27191
27959
  ...extra
27192
27960
  });
27193
- return true;
27961
+ return settledIndex >= 0;
27194
27962
  };
27195
27963
  return { patchItem, settleStreamingTail };
27196
27964
  }
@@ -27239,6 +28007,7 @@ async function createEngineSession({
27239
28007
  const pendingNotificationKeys = /* @__PURE__ */ new Set();
27240
28008
  const displayedExecutionNotificationKeys = /* @__PURE__ */ new Set();
27241
28009
  const bag = {};
28010
+ let state;
27242
28011
  const {
27243
28012
  autoClearState,
27244
28013
  agentStatusState,
@@ -27248,6 +28017,9 @@ async function createEngineSession({
27248
28017
  } = createContextState({
27249
28018
  runtime,
27250
28019
  getState: () => state,
28020
+ updateState: (patch) => {
28021
+ state = { ...state, ...patch };
28022
+ },
27251
28023
  getPendingSessionReset: () => flags.pendingSessionReset
27252
28024
  });
27253
28025
  const initialAgentState = {
@@ -27255,8 +28027,12 @@ async function createEngineSession({
27255
28027
  agentJobs: [],
27256
28028
  agentScope: null
27257
28029
  };
27258
- let state = {
28030
+ state = {
27259
28031
  items: [],
28032
+ transcriptViewItems: null,
28033
+ transcriptViewRevision: 0,
28034
+ transcriptHistoryBefore: false,
28035
+ transcriptHistoryAfter: false,
27260
28036
  structureRevision: 0,
27261
28037
  streamingTail: null,
27262
28038
  toasts: [],
@@ -27295,50 +28071,109 @@ async function createEngineSession({
27295
28071
  syncContextStats({ allowEstimated: true });
27296
28072
  bootProfile("engine:context-ready", { ms: (performance2.now() - contextStartedAt).toFixed(1) });
27297
28073
  const listeners = /* @__PURE__ */ new Set();
27298
- let emitScheduled = false;
27299
- const flushEmit = () => {
27300
- emitScheduled = false;
27301
- for (const l of listeners) l();
27302
- };
27303
- const emit = () => {
27304
- if (emitScheduled) return;
27305
- emitScheduled = true;
27306
- queueMicrotask(flushEmit);
27307
- };
28074
+ let publishedState = process.env.NODE_ENV === "production" ? state : Object.freeze(state);
28075
+ state = { ...state, stats: { ...state.stats } };
28076
+ const publisher = createFrameBatchedStorePublisher({
28077
+ getState: () => state,
28078
+ publishState: (next) => {
28079
+ publishedState = process.env.NODE_ENV === "production" ? next : Object.freeze(next);
28080
+ state = { ...next, stats: { ...next.stats } };
28081
+ },
28082
+ listeners,
28083
+ isDisposed: () => flags.disposed
28084
+ });
28085
+ const emit = publisher.emit;
28086
+ const flushEmit = publisher.flush;
28087
+ const flushEmitImmediate = publisher.flushImmediate;
27308
28088
  const set = (patch) => {
27309
28089
  if (!patch || typeof patch !== "object") return false;
28090
+ const requestsStructureChange = Object.prototype.hasOwnProperty.call(patch, "structureRevision") && !Object.is(patch.structureRevision, state.structureRevision);
28091
+ const effectivePatch = requestsStructureChange ? Object.fromEntries(Object.entries(patch).filter(([key]) => key !== "structureRevision")) : patch;
27310
28092
  let changed = false;
27311
- for (const [key, value] of Object.entries(patch)) {
28093
+ for (const [key, value] of Object.entries(effectivePatch)) {
27312
28094
  if (!Object.is(state[key], value)) {
27313
28095
  changed = true;
27314
28096
  break;
27315
28097
  }
27316
28098
  }
27317
- if (!changed) return false;
28099
+ if (!changed && !requestsStructureChange) return false;
27318
28100
  const commandBusyReleased = state.commandBusy === true && Object.prototype.hasOwnProperty.call(patch, "commandBusy") && patch.commandBusy === false;
27319
- state = { ...state, ...patch };
28101
+ state = { ...state, ...effectivePatch };
28102
+ if (requestsStructureChange) publisher.markStructureChange();
27320
28103
  emit();
28104
+ if (effectivePatch.commandStatus || effectivePatch.toolApproval) {
28105
+ flushEmitImmediate();
28106
+ }
27321
28107
  if (commandBusyReleased) queueMicrotask(() => {
27322
28108
  void bag.drain?.();
27323
28109
  });
27324
28110
  return true;
27325
28111
  };
27326
28112
  const itemIndexById = /* @__PURE__ */ new Map();
27327
- const replaceItems = (items, { preserveStreamingTail = false } = {}) => {
28113
+ const transcriptSpill = createTranscriptSpillBuffer();
28114
+ const reindexLiveItems = (items) => {
28115
+ itemIndexById.clear();
28116
+ for (let i = 0; i < items.length; i++) {
28117
+ const id = items[i]?.id;
28118
+ if (id != null) itemIndexById.set(id, i);
28119
+ }
28120
+ };
28121
+ const transcriptHistoryFlags = () => ({
28122
+ transcriptHistoryBefore: transcriptSpill.hasOlder,
28123
+ transcriptHistoryAfter: transcriptSpill.hasNewer
28124
+ });
28125
+ const restoreOlderTranscript = () => {
28126
+ const transcriptViewItems = transcriptSpill.restoreOlder(state.items);
28127
+ if (!transcriptViewItems) return false;
28128
+ set({
28129
+ transcriptViewItems,
28130
+ transcriptViewRevision: state.transcriptViewRevision + 1,
28131
+ ...transcriptHistoryFlags()
28132
+ });
28133
+ flushEmitImmediate();
28134
+ return true;
28135
+ };
28136
+ const restoreNewerTranscript = () => {
28137
+ const restored = transcriptSpill.restoreNewer(state.items);
28138
+ if (!restored) return false;
28139
+ set({
28140
+ transcriptViewItems: restored.atLive ? null : restored,
28141
+ transcriptViewRevision: state.transcriptViewRevision + 1,
28142
+ ...transcriptHistoryFlags()
28143
+ });
28144
+ flushEmitImmediate();
28145
+ return true;
28146
+ };
28147
+ const replaceItems = (items, {
28148
+ preserveStreamingTail = false,
28149
+ preserveSpill = false,
28150
+ preserveTranscriptView = false
28151
+ } = {}) => {
27328
28152
  const nextItems = Array.isArray(items) ? items : [];
28153
+ if (!preserveSpill) transcriptSpill.reset();
28154
+ const liveItems = transcriptSpill.capLive(nextItems);
28155
+ const previousTranscriptView = state.transcriptViewItems;
28156
+ const nextTranscriptView = preserveTranscriptView && previousTranscriptView ? refillTranscriptViewOverlap(previousTranscriptView, state.items, liveItems) : null;
28157
+ const transcriptViewChanged = nextTranscriptView !== previousTranscriptView;
27329
28158
  activeToolCalls.clear();
28159
+ const structureRevision = state.structureRevision;
27330
28160
  state = replaceEngineItemsState({
27331
28161
  state,
27332
- items: nextItems,
28162
+ items: liveItems,
27333
28163
  itemIndexById,
27334
28164
  preserveStreamingTail,
27335
28165
  extra: {
27336
- promptHistoryList: buildMergedPromptHistory(recomputePromptHistory(nextItems), loadPromptHistory(state.cwd)),
27337
- activeToolSummary: null
28166
+ promptHistoryList: preserveSpill ? state.promptHistoryList : buildMergedPromptHistory(recomputePromptHistory(nextItems), loadPromptHistory(state.cwd)),
28167
+ activeToolSummary: null,
28168
+ transcriptViewItems: nextTranscriptView,
28169
+ transcriptViewRevision: preserveTranscriptView ? state.transcriptViewRevision + (transcriptViewChanged ? 1 : 0) : state.transcriptViewRevision + 1,
28170
+ ...transcriptHistoryFlags()
27338
28171
  }
27339
28172
  });
28173
+ state = { ...state, structureRevision };
28174
+ publisher.markStructureChange();
27340
28175
  emit();
27341
- return nextItems;
28176
+ return liveItems;
27342
28177
  };
27343
28178
  const activeToolCalls = /* @__PURE__ */ new Map();
27344
28179
  const recomputeActiveToolSummary = () => {
@@ -27378,16 +28213,30 @@ async function createEngineSession({
27378
28213
  if (!flags.pushingFromDeferredEntry && flags.flushDeferredBeforeImmediatePush) {
27379
28214
  flags.flushDeferredBeforeImmediatePush();
27380
28215
  }
27381
- const index = state.items.length;
27382
- const items = [...state.items, item];
28216
+ const uncappedItems = [...state.items, item];
28217
+ const items = transcriptSpill.capLive(uncappedItems);
28218
+ if (items !== uncappedItems) reindexLiveItems(items);
28219
+ const index = items.length - 1;
27383
28220
  if (item?.id != null) itemIndexById.set(item.id, index);
27384
28221
  if (item?.kind === "user") {
27385
28222
  const promptHistoryList = buildMergedPromptHistory(recomputePromptHistory(items), loadPromptHistory(state.cwd));
27386
- set({ items, structureRevision: state.structureRevision + 1, promptHistoryList });
28223
+ set({ items, structureRevision: state.structureRevision + 1, promptHistoryList, ...transcriptHistoryFlags() });
28224
+ flushEmitImmediate();
27387
28225
  } else {
27388
- set({ items, structureRevision: state.structureRevision + 1 });
28226
+ set({ items, structureRevision: state.structureRevision + 1, ...transcriptHistoryFlags() });
27389
28227
  }
27390
28228
  };
28229
+ const appendItems = (newItems, extra = {}) => {
28230
+ if (!Array.isArray(newItems) || newItems.length === 0) return set(extra);
28231
+ const items = transcriptSpill.capLive([...state.items, ...newItems]);
28232
+ reindexLiveItems(items);
28233
+ return set({
28234
+ items,
28235
+ structureRevision: state.structureRevision + 1,
28236
+ ...transcriptHistoryFlags(),
28237
+ ...extra
28238
+ });
28239
+ };
27391
28240
  const updateStreamingTail = (id, patch = {}, extra = {}) => {
27392
28241
  if (id == null) return false;
27393
28242
  const current = state.streamingTail?.id === id ? state.streamingTail : { kind: "assistant", id, text: "", streaming: true };
@@ -27412,7 +28261,9 @@ async function createEngineSession({
27412
28261
  const { patchItem, settleStreamingTail } = createEngineItemMutators({
27413
28262
  getState: () => state,
27414
28263
  set,
27415
- itemIndexById
28264
+ itemIndexById,
28265
+ normalizeItems: (items) => transcriptSpill.capLive(items),
28266
+ itemStateExtra: transcriptHistoryFlags
27416
28267
  });
27417
28268
  const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
27418
28269
  const synthetic = parseSyntheticAgentMessage(text);
@@ -27626,10 +28477,17 @@ async function createEngineSession({
27626
28477
  listeners,
27627
28478
  itemIndexById,
27628
28479
  getState: () => state,
28480
+ getPublishedState: () => publishedState,
27629
28481
  set,
28482
+ flushEmit,
28483
+ flushEmitImmediate,
28484
+ disposeEmit: publisher.dispose,
27630
28485
  pushItem,
28486
+ appendItems,
27631
28487
  patchItem,
27632
28488
  replaceItems,
28489
+ restoreOlderTranscript,
28490
+ restoreNewerTranscript,
27633
28491
  updateStreamingTail,
27634
28492
  settleStreamingTail,
27635
28493
  clearStreamingTail,
@@ -27649,6 +28507,10 @@ async function createEngineSession({
27649
28507
  baseRouteState,
27650
28508
  routeState,
27651
28509
  syncContextStats,
28510
+ disposeTranscriptSpill: () => transcriptSpill.dispose(),
28511
+ snapshotTranscriptSpill: () => transcriptSpill.snapshot(),
28512
+ restoreTranscriptSpill: (snapshot) => transcriptSpill.restoreSnapshot(snapshot),
28513
+ releaseTranscriptSpill: (snapshot) => transcriptSpill.releaseSnapshot(snapshot),
27652
28514
  presentNextToolApproval,
27653
28515
  finishToolApproval,
27654
28516
  denyAllToolApprovals,
@@ -27891,7 +28753,7 @@ function scheduleHardExit(code = 0) {
27891
28753
  timer2.unref?.();
27892
28754
  }
27893
28755
  function resolveTuiStderrLogPath() {
27894
- return process.env.MIXDOG_TUI_STDERR_LOG || join10(process.env.MIXDOG_RUNTIME_ROOT || join10(tmpdir2(), "mixdog"), "mixdog-tui.stderr.log");
28756
+ return process.env.MIXDOG_TUI_STDERR_LOG || join11(process.env.MIXDOG_RUNTIME_ROOT || join11(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
27895
28757
  }
27896
28758
  function ansiFg(rgb) {
27897
28759
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));