mixdog 0.9.17 → 0.9.18

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.
@@ -19,6 +19,7 @@ import {
19
19
  buildTranscriptRowIndexIncremental,
20
20
  transcriptStructureSignature,
21
21
  estimateTranscriptItemRowsCached,
22
+ setStreamingBottomPinned,
22
23
  transcriptRenderWindow,
23
24
  resolveAnchorScrollOffset,
24
25
  upperBound,
@@ -148,6 +149,14 @@ export function useTranscriptWindow({
148
149
  ),
149
150
  [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded],
150
151
  );
152
+ const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
153
+ const transcriptPinnedForStreaming = followingRef.current
154
+ || scrolledUpRowsForPin <= transcriptBottomSlackRows;
155
+ // Publish the pinned state to the streaming-tail estimator BEFORE resolving
156
+ // tailSig / the row-index memo this render, so max(measuredFloor, liveEstimate)
157
+ // applies while bottom-pinned (right geometry on first commit, no judder) and
158
+ // every tail-resolving path reads the same height (no sig/geometry divergence).
159
+ setStreamingBottomPinned(transcriptPinnedForStreaming);
151
160
  const tailSig = streamingTailItem
152
161
  ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}`
153
162
  : '_';
@@ -155,9 +164,6 @@ export function useTranscriptWindow({
155
164
  const transcriptStreamingActive = (items || []).some(
156
165
  (item) => item?.kind === 'assistant' && item?.streaming,
157
166
  );
158
- const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
159
- const transcriptPinnedForStreaming = followingRef.current
160
- || scrolledUpRowsForPin <= transcriptBottomSlackRows;
161
167
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
162
168
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
163
169
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current
@@ -11100,6 +11100,10 @@ function measuredTranscriptRows(item, columns, toolOutputExpanded) {
11100
11100
  return entry.rows;
11101
11101
  }
11102
11102
  var STREAMING_ROW_QUANTUM = 1;
11103
+ var streamingBottomPinned = false;
11104
+ function setStreamingBottomPinned(pinned) {
11105
+ streamingBottomPinned = !!pinned;
11106
+ }
11103
11107
  function assistantTextForStreamingRowEstimate(text) {
11104
11108
  return streamingLayoutText(text);
11105
11109
  }
@@ -11116,6 +11120,9 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
11116
11120
  const toolExpanded2 = toolOutputExpanded ? 1 : 0;
11117
11121
  const idEntry = streamingMeasuredRowsById.get(item.id);
11118
11122
  if (idEntry && idEntry.rows > 0 && idEntry.columns === columns && idEntry.toolExpanded === toolExpanded2) {
11123
+ if (streamingBottomPinned) {
11124
+ return Math.max(idEntry.rows, streamingEstimateRows(item, columns, toolOutputExpanded));
11125
+ }
11119
11126
  return idEntry.rows;
11120
11127
  }
11121
11128
  if (idEntry) streamingMeasuredRowsById.delete(item.id);
@@ -12081,13 +12088,14 @@ function useTranscriptWindow({
12081
12088
  ),
12082
12089
  [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded]
12083
12090
  );
12091
+ const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
12092
+ const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin <= transcriptBottomSlackRows;
12093
+ setStreamingBottomPinned(transcriptPinnedForStreaming);
12084
12094
  const tailSig = streamingTailItem ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}` : "_";
12085
12095
  const transcriptStructureSig = `${prefixSig}#${tailSig}`;
12086
12096
  const transcriptStreamingActive = (items || []).some(
12087
12097
  (item) => item?.kind === "assistant" && item?.streaming
12088
12098
  );
12089
- const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
12090
- const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin <= transcriptBottomSlackRows;
12091
12099
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
12092
12100
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
12093
12101
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current || transcriptAnchorDirtyRef.current;
@@ -23054,13 +23062,20 @@ async function createEngineSession({
23054
23062
  seq: deferredSeqCounter++,
23055
23063
  pushed: false,
23056
23064
  timer: null,
23057
- push: () => {
23065
+ // Mark the card visible and return its spec WITHOUT emitting, so a
23066
+ // batched turn-close flush can commit many specs in one set().
23067
+ materialize: () => {
23058
23068
  card.pushed = true;
23059
- if (!card.spec) return;
23069
+ if (!card.spec) return null;
23060
23070
  card.spec.deferredDisplayReady = true;
23071
+ return card.spec;
23072
+ },
23073
+ push: () => {
23074
+ const spec = entry.materialize();
23075
+ if (!spec) return;
23061
23076
  pushingFromDeferredEntry = true;
23062
23077
  try {
23063
- pushItem(card.spec);
23078
+ pushItem(spec);
23064
23079
  } finally {
23065
23080
  pushingFromDeferredEntry = false;
23066
23081
  }
@@ -23081,13 +23096,18 @@ async function createEngineSession({
23081
23096
  seq: deferredSeqCounter++,
23082
23097
  pushed: false,
23083
23098
  timer: null,
23084
- push: () => {
23099
+ materialize: () => {
23085
23100
  aggregate.pushed = true;
23086
- if (!aggregate.pendingSpec) return;
23101
+ if (!aggregate.pendingSpec) return null;
23087
23102
  aggregate.pendingSpec.deferredDisplayReady = true;
23103
+ return aggregate.pendingSpec;
23104
+ },
23105
+ push: () => {
23106
+ const spec = entry.materialize();
23107
+ if (!spec) return;
23088
23108
  pushingFromDeferredEntry = true;
23089
23109
  try {
23090
- pushItem(aggregate.pendingSpec);
23110
+ pushItem(spec);
23091
23111
  } finally {
23092
23112
  pushingFromDeferredEntry = false;
23093
23113
  }
@@ -23111,6 +23131,35 @@ async function createEngineSession({
23111
23131
  }
23112
23132
  }
23113
23133
  };
23134
+ const collectDeferredUpTo = (entry) => {
23135
+ const specs = [];
23136
+ if (!entry) return specs;
23137
+ for (const e of deferredEntries) {
23138
+ if (e.seq > entry.seq) break;
23139
+ if (e.pushed) continue;
23140
+ e.pushed = true;
23141
+ if (e.timer) {
23142
+ clearTimeout(e.timer);
23143
+ e.timer = null;
23144
+ }
23145
+ const spec = e.materialize?.();
23146
+ if (spec) specs.push(spec);
23147
+ }
23148
+ return specs;
23149
+ };
23150
+ const appendItemsBatch = (newItems, extra = {}) => {
23151
+ if (!newItems || !newItems.length) {
23152
+ set(extra);
23153
+ return;
23154
+ }
23155
+ const base = state.items.length;
23156
+ const items = [...state.items, ...newItems];
23157
+ for (let i = 0; i < newItems.length; i++) {
23158
+ const it = newItems[i];
23159
+ if (it?.id != null) itemIndexById.set(it.id, base + i);
23160
+ }
23161
+ set({ items, ...extra });
23162
+ };
23114
23163
  const markPromptCommitted = () => {
23115
23164
  if (activePromptRestore) {
23116
23165
  if (!promptCommittedCallbackCalled && typeof activePromptRestore.onCommitted === "function") {
@@ -23613,13 +23662,14 @@ async function createEngineSession({
23613
23662
  denyAllToolApprovals(cancelled ? "turn cancelled" : "turn finished");
23614
23663
  clearWatchdog();
23615
23664
  const isStaleUnwind = leadTurnEpoch !== turnEpoch;
23665
+ let closingItems = [];
23616
23666
  if (deferredEntries.length) {
23617
23667
  const last = deferredEntries[deferredEntries.length - 1];
23618
- if (last) flushDeferredUpTo(last);
23668
+ closingItems = collectDeferredUpTo(last);
23619
23669
  clearDeferredTimers();
23620
23670
  }
23621
23671
  flushDeferredBeforeImmediatePush = null;
23622
- const producedTranscriptItem = state.items.length > itemsAtTurnStart;
23672
+ const producedTranscriptItem = state.items.length + closingItems.length > itemsAtTurnStart;
23623
23673
  const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
23624
23674
  if (!isStaleUnwind) activePromptRestore = null;
23625
23675
  closeThinkingSegment();
@@ -23633,15 +23683,16 @@ async function createEngineSession({
23633
23683
  const assistantOutput = (currentAssistantText || assistantText || "").trim();
23634
23684
  const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
23635
23685
  if (isStaleUnwind) {
23686
+ if (closingItems.length) appendItemsBatch(closingItems);
23636
23687
  tuiDebug(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared-state writes`);
23637
23688
  } else {
23638
23689
  if (!isNoOpTurn) {
23639
23690
  state.stats.turns = (state.stats.turns || 0) + 1;
23640
23691
  }
23641
23692
  if (!reclaimed && !isNoOpTurn) {
23642
- pushItem({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23693
+ closingItems.push({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23643
23694
  }
23644
- set({
23695
+ appendItemsBatch(closingItems, {
23645
23696
  busy: false,
23646
23697
  spinner: null,
23647
23698
  thinking: null,
@@ -775,12 +775,19 @@ export async function createEngineSession({
775
775
  seq: deferredSeqCounter++,
776
776
  pushed: false,
777
777
  timer: null,
778
- push: () => {
778
+ // Mark the card visible and return its spec WITHOUT emitting, so a
779
+ // batched turn-close flush can commit many specs in one set().
780
+ materialize: () => {
779
781
  card.pushed = true;
780
- if (!card.spec) return;
782
+ if (!card.spec) return null;
781
783
  card.spec.deferredDisplayReady = true;
784
+ return card.spec;
785
+ },
786
+ push: () => {
787
+ const spec = entry.materialize();
788
+ if (!spec) return;
782
789
  pushingFromDeferredEntry = true;
783
- try { pushItem(card.spec); } finally { pushingFromDeferredEntry = false; }
790
+ try { pushItem(spec); } finally { pushingFromDeferredEntry = false; }
784
791
  },
785
792
  };
786
793
  card.deferred = entry;
@@ -798,12 +805,17 @@ export async function createEngineSession({
798
805
  seq: deferredSeqCounter++,
799
806
  pushed: false,
800
807
  timer: null,
801
- push: () => {
808
+ materialize: () => {
802
809
  aggregate.pushed = true;
803
- if (!aggregate.pendingSpec) return;
810
+ if (!aggregate.pendingSpec) return null;
804
811
  aggregate.pendingSpec.deferredDisplayReady = true;
812
+ return aggregate.pendingSpec;
813
+ },
814
+ push: () => {
815
+ const spec = entry.materialize();
816
+ if (!spec) return;
805
817
  pushingFromDeferredEntry = true;
806
- try { pushItem(aggregate.pendingSpec); } finally { pushingFromDeferredEntry = false; }
818
+ try { pushItem(spec); } finally { pushingFromDeferredEntry = false; }
807
819
  },
808
820
  };
809
821
  aggregate.deferred = entry;
@@ -821,6 +833,35 @@ export async function createEngineSession({
821
833
  if (e.timer) { clearTimeout(e.timer); e.timer = null; }
822
834
  }
823
835
  };
836
+ // Collect (mark pushed + cancel timers) every still-deferred entry up to
837
+ // `entry` in creation order, returning their specs WITHOUT emitting — the
838
+ // caller commits them (optionally alongside a trailing turndone item) in a
839
+ // single set() so turn-close writes land as ONE visible commit.
840
+ const collectDeferredUpTo = (entry) => {
841
+ const specs = [];
842
+ if (!entry) return specs;
843
+ for (const e of deferredEntries) {
844
+ if (e.seq > entry.seq) break;
845
+ if (e.pushed) continue;
846
+ e.pushed = true;
847
+ if (e.timer) { clearTimeout(e.timer); e.timer = null; }
848
+ const spec = e.materialize?.();
849
+ if (spec) specs.push(spec);
850
+ }
851
+ return specs;
852
+ };
853
+ // Append pre-built items (deferred cards + turndone) in ONE set(). None are
854
+ // 'user' kind, so no promptHistory rebuild is needed.
855
+ const appendItemsBatch = (newItems, extra = {}) => {
856
+ if (!newItems || !newItems.length) { set(extra); return; }
857
+ const base = state.items.length;
858
+ const items = [...state.items, ...newItems];
859
+ for (let i = 0; i < newItems.length; i++) {
860
+ const it = newItems[i];
861
+ if (it?.id != null) itemIndexById.set(it.id, base + i);
862
+ }
863
+ set({ items, ...extra });
864
+ };
824
865
 
825
866
  const markPromptCommitted = () => {
826
867
  if (activePromptRestore) {
@@ -1542,13 +1583,18 @@ export async function createEngineSession({
1542
1583
  // pending push timers so nothing fires (or leaks) after the turn ends. The
1543
1584
  // finalize path above already patches results onto visible cards; this just
1544
1585
  // guarantees every registered card is materialized before the turn closes.
1586
+ // Collect (don't emit) the still-deferred cards so the turn-close flush and
1587
+ // the turndone item append in ONE set() below instead of one render bounce
1588
+ // per row. Order/ids are preserved (creation order, then turndone last).
1589
+ let closingItems = [];
1545
1590
  if (deferredEntries.length) {
1546
1591
  const last = deferredEntries[deferredEntries.length - 1];
1547
- if (last) flushDeferredUpTo(last);
1592
+ closingItems = collectDeferredUpTo(last);
1548
1593
  clearDeferredTimers();
1549
1594
  }
1550
1595
  flushDeferredBeforeImmediatePush = null;
1551
- const producedTranscriptItem = state.items.length > itemsAtTurnStart;
1596
+ const producedTranscriptItem =
1597
+ state.items.length + closingItems.length > itemsAtTurnStart;
1552
1598
  const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
1553
1599
  if (!isStaleUnwind) activePromptRestore = null;
1554
1600
  closeThinkingSegment();
@@ -1574,6 +1620,9 @@ export async function createEngineSession({
1574
1620
  && !assistantOutput
1575
1621
  && !producedTranscriptItem;
1576
1622
  if (isStaleUnwind) {
1623
+ // Preserve prior behavior: a stale unwind still materializes its own
1624
+ // deferred cards (turn-local teardown) but writes no other shared state.
1625
+ if (closingItems.length) appendItemsBatch(closingItems);
1577
1626
  tuiDebug(`runTurn STALE UNWIND turn=${turnIndex} — force-released; skipping shared-state writes`);
1578
1627
  } else {
1579
1628
  if (!isNoOpTurn) {
@@ -1584,9 +1633,10 @@ export async function createEngineSession({
1584
1633
  // in scrollback. (Previously TurnDone rendered only in the
1585
1634
  // bottom-fixed live-status slot and vanished on the next turn.)
1586
1635
  if (!reclaimed && !isNoOpTurn) {
1587
- pushItem({ kind: 'turndone', id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
1636
+ closingItems.push({ kind: 'turndone', id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
1588
1637
  }
1589
- set({
1638
+ // Deferred cards + turndone + status all land in ONE set() (one commit).
1639
+ appendItemsBatch(closingItems, {
1590
1640
  busy: false,
1591
1641
  spinner: null,
1592
1642
  thinking: null,