mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -12,7 +12,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
12
12
  import {
13
13
  TRANSCRIPT_MEASURED_ROWS,
14
14
  transcriptMeasuredRowsCache,
15
- streamingMeasuredRowsById,
16
15
  pruneStreamingMeasuredRowsById,
17
16
  hasStreamingRowStateToPrune,
18
17
  transcriptItemVariantKey,
@@ -21,12 +20,8 @@ import {
21
20
  transcriptRowAt,
22
21
  buildTranscriptRowIndexIncremental,
23
22
  estimateTranscriptItemRowsCached,
24
- setStreamingBottomPinned,
25
23
  transcriptRenderWindow,
26
24
  resolveAnchorScrollOffset,
27
- streamingTailMountedGrowth,
28
- streamingEstimateRows,
29
- TRANSCRIPT_WINDOW_OVERSCAN_ROWS,
30
25
  upperBound,
31
26
  shiftSelectionRectY,
32
27
  transcriptHarvestInputsEqual,
@@ -59,6 +54,7 @@ function traceHarvestBreaker(changedKeys) {
59
54
  export function useTranscriptWindow({
60
55
  items: settledItems,
61
56
  structureRevision,
57
+ sessionKey = '',
62
58
  streamingTail,
63
59
  themeEpoch,
64
60
  frameColumns,
@@ -141,6 +137,46 @@ export function useTranscriptWindow({
141
137
  // item by id at call time. The ref(null) path drops the element; the harvest
142
138
  // reads getComputedHeight from whatever is mounted, so an unmount simply stops
143
139
  // contributing (its last measurement stays cached on the item object).
140
+ const normalizedSessionKey = String(sessionKey ?? '');
141
+ const transcriptSessionKeyRef = useRef(normalizedSessionKey);
142
+ if (transcriptSessionKeyRef.current !== normalizedSessionKey) {
143
+ // A resumed session replaces the transcript atomically, but this hook
144
+ // remains mounted. Never let the outgoing session's row index, reading
145
+ // anchor, measured-element map, or bottom-relative offset participate in
146
+ // the incoming session's first frame. Reset synchronously during render so
147
+ // the new transcript paints at its final bottom-pinned position; the layout
148
+ // effect below only reconciles the App-owned scroll state afterward.
149
+ transcriptSessionKeyRef.current = normalizedSessionKey;
150
+ transcriptTotalRowsRef.current = 0;
151
+ harvestBumpStreakRef.current = { count: 0, timer: null };
152
+ committedMaxScrollRowsRef.current = 0;
153
+ incrementalRowIndexCacheRef.current = null;
154
+ transcriptItemsCacheRef.current = null;
155
+ prevViewportGeomRef.current = { contentHeight: 0, floatingPanelRows: 0 };
156
+ transcriptItemElsRef.current.clear();
157
+ transcriptMeasureRefCache.current.clear();
158
+ transcriptMeasureItemsRef.current.clear();
159
+ harvestGateRef.current = {
160
+ inputs: null,
161
+ skippedForDrag: false,
162
+ forceNext: false,
163
+ };
164
+ transcriptAnchorRef.current = null;
165
+ transcriptAnchorDirtyRef.current = false;
166
+ scrollTargetRef.current = 0;
167
+ scrollPositionRef.current = 0;
168
+ maxScrollRowsRef.current = 0;
169
+ transcriptGeomRef.current = {};
170
+ followingRef.current = true;
171
+ selectionLayoutRef.current = null;
172
+ if (hasStreamingRowStateToPrune()) {
173
+ pruneStreamingMeasuredRowsById(new Set());
174
+ }
175
+ }
176
+ useLayoutEffect(() => {
177
+ if (scrollOffset !== 0) setScrollOffset(0);
178
+ }, [normalizedSessionKey, setScrollOffset]);
179
+
144
180
  const transcriptMeasureRef = useCallback((item) => {
145
181
  if (!TRANSCRIPT_MEASURED_ROWS || !item || item.id == null) return undefined;
146
182
  if (shouldSuppressFullyFailedToolItem(item)) {
@@ -172,13 +208,7 @@ export function useTranscriptWindow({
172
208
  ? streamingTail
173
209
  : null;
174
210
  const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
175
- const transcriptPinnedForStreaming = followingRef.current
176
- || scrolledUpRowsForPin === 0;
177
- // Publish the pinned state to the streaming-tail estimator BEFORE resolving
178
- // tailSig / the row-index memo this render, so max(measuredFloor, liveEstimate)
179
- // applies while bottom-pinned (right geometry on first commit, no judder) and
180
- // every tail-resolving path reads the same height (no sig/geometry divergence).
181
- setStreamingBottomPinned(transcriptPinnedForStreaming);
211
+ const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin === 0;
182
212
  const tailRows = streamingTailItem
183
213
  ? estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)
184
214
  : 0;
@@ -381,41 +411,6 @@ export function useTranscriptWindow({
381
411
  contentHeight: lockViewRows,
382
412
  floatingPanelRows: Number(floatingPanelRows) || 0,
383
413
  };
384
- // ── Same-frame streaming-tail growth compensation (scrolled-up only) ───────
385
- // When the user reads older transcript, the row index freezes the trailing
386
- // streaming item at its last Yoga-confirmed height (defer-growth), but the
387
- // mounted Box lays out at the live-grown height THIS frame. renderScrollOffset
388
- // was just resolved against that frozen row index, so the extra physical rows
389
- // are unaccounted-for and the visible window jumps up one frame until the
390
- // harvest bumps measuredRowsVersion. Fold the mounted growth delta into the
391
- // offset NOW so marginBottom slides those extra rows off the bottom and the
392
- // window holds still. Bottom-pinned/following is stable (flex-end owns it) and
393
- // is excluded by the scrolledUp gate. No double-compensation next frame: once
394
- // measuredRowsVersion bumps, the row index absorbs the growth (idEntry.rows ==
395
- // the new measured height) and the live estimate matches it, so delta → 0.
396
- if (scrolledUp && !followingRef.current) {
397
- const growth = streamingTailMountedGrowth(
398
- streamingTailItem ? [streamingTailItem] : transcriptItems,
399
- frameColumns,
400
- toolOutputExpanded,
401
- );
402
- // Only compensate when the tail is actually mounted in the rendered slice
403
- // (viewport + overscan). Off-slice it is represented by a row-index-sized
404
- // bottom spacer that does NOT physically grow this frame, so shifting the
405
- // offset there would itself introduce a jump. In-slice its Box grows now.
406
- // Gate/clamp on the POST-compensation offset: transcriptRenderWindow drops
407
- // the tail once effectiveScrollOffset >= tailRows + overscan, so adding the
408
- // full delta could push the offset past that edge and unmount the very row
409
- // we are compensating for (a reverse jump at the boundary). Cap the applied
410
- // delta so the final offset keeps the tail mounted; any residual growth left
411
- // uncompensated sits in the overscan below the viewport (not visible).
412
- if (growth && growth.delta > 0) {
413
- const maxOffsetKeepingTailMounted = growth.tailRows + TRANSCRIPT_WINDOW_OVERSCAN_ROWS - 1;
414
- if (renderScrollOffset <= maxOffsetKeepingTailMounted) {
415
- renderScrollOffset += Math.min(growth.delta, maxOffsetKeepingTailMounted - renderScrollOffset);
416
- }
417
- }
418
- }
419
414
  const transcriptWindow = useMemo(() => transcriptRenderWindow(transcriptItems, {
420
415
  scrollOffset: renderScrollOffset,
421
416
  viewportHeight: transcriptContentHeight,
@@ -447,12 +442,10 @@ export function useTranscriptWindow({
447
442
  for (let i = 0; i < mountedSlice.length; i++) {
448
443
  const it = mountedSlice[i];
449
444
  if (!it || shouldSuppressFullyFailedToolItem(it)) continue;
450
- if (it.kind === 'assistant' && it.streaming) {
451
- const idPrev = streamingMeasuredRowsById.get(it.id);
452
- if (!idPrev || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
453
- holdCommittedMax = true;
454
- break;
455
- }
445
+ if (it.kind === 'assistant') {
446
+ // Assistant rows are exact-model owned and never wait for a Yoga
447
+ // commit before their max scroll range becomes authoritative.
448
+ continue;
456
449
  } else {
457
450
  const prev = transcriptMeasuredRowsCache.get(it);
458
451
  if (!prev
@@ -580,14 +573,9 @@ export function useTranscriptWindow({
580
573
  if (transcriptMeasuredRowsCache.delete(item)) { changed = true; changedKeys.push(`${key}=del`); }
581
574
  continue;
582
575
  }
583
- // Streaming assistant rows are now harvested too: their height churns
584
- // every flush, but skipping them entirely meant the measured cache
585
- // never observed live re-slice growth, so anchor-lock signatures
586
- // (transcriptStructureSignature) could not react to it. Since streaming
587
- // items are replaced per-token, their WeakMap entry is orphaned every
588
- // frame; the id-keyed streamingMeasuredRowsById store (transcript-
589
- // window.mjs) is written below to survive that swap. The drag guard
590
- // above keeps this from fighting drag scroll.
576
+ // Active stream geometry is owned exclusively by the deterministic row
577
+ // model. Harvesting it here would create a second, post-commit geometry
578
+ // authority and reintroduce estimate→measure correction jitter.
591
579
  // Width 0 = Yoga has not laid this node out yet this frame; skip so a
592
580
  // transient 0 never poisons the cache (guard on a real positive width).
593
581
  if (typeof yoga.getComputedWidth === 'function' && yoga.getComputedWidth() <= 0) continue;
@@ -596,48 +584,14 @@ export function useTranscriptWindow({
596
584
  if (transcriptMeasuredRowsCache.delete(item)) { changed = true; changedKeys.push(`${key}=del`); }
597
585
  continue;
598
586
  }
599
- const isStreamingAssistant = item.kind === 'assistant' && !!item.streaming;
587
+ if (item.kind === 'assistant') {
588
+ transcriptMeasuredRowsCache.delete(item);
589
+ continue;
590
+ }
600
591
  const measured = Math.max(1, rawMeasured);
601
592
  const variantKey = transcriptItemVariantKey(item);
602
- if (isStreamingAssistant) {
603
- const idPrev = streamingMeasuredRowsById.get(item.id);
604
- // Baseline = the estimate for the text at THIS commit's measured state.
605
- // Refresh it on EVERY harvest pass (not only on an id-store change): if
606
- // the estimator's row count drifts while the Yoga height is unchanged
607
- // (or a same-id text reset), a stale baseline would make
608
- // streamingTailMountedGrowth compensate with no physical growth.
609
- const estimateAtMeasure = streamingEstimateRows(item, frameColumns, toolOutputExpanded);
610
- if (!idPrev || idPrev.rows !== measured || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
611
- // Record the estimate for the EXACT text just measured alongside the
612
- // real Yoga height, so streamingTailMountedGrowth can measure later
613
- // growth in the estimate metric (live_now − estimateAtMeasure) and the
614
- // estimator's steady bias cancels instead of leaking into a permanent
615
- // over-compensation once measuredRowsVersion has absorbed this height.
616
- streamingMeasuredRowsById.set(item.id, {
617
- rows: measured,
618
- columns: frameColumns,
619
- toolExpanded: toolExpandedFlag,
620
- estimateRows: estimateAtMeasure,
621
- });
622
- // ALWAYS bump on a real id-store change, bottom-pinned or not.
623
- // estimateTranscriptItemRowsCached (transcript-window.mjs) now
624
- // returns ONLY this id-store floor for a streaming item (defer-
625
- // growth: no live-estimate blend), so it is the SOLE path that ever
626
- // feeds this item's grown height into transcriptStructureSig /
627
- // buildTranscriptRowIndex. Skipping the bump while bottom-pinned
628
- // used to be safe because the blended estimate kept the signature
629
- // moving on its own each flush; with the blend gone, skipping it
630
- // here would freeze totalRows/prefixRows for this item until the
631
- // stream settles — invisible growth for anything reading rowIndex
632
- // (windowing, maxScrollRows, an anchor captured mid-stream).
633
- changed = true;
634
- changedKeys.push(`${key}=${measured}s`);
635
- } else {
636
- idPrev.estimateRows = estimateAtMeasure;
637
- }
638
- }
639
593
  const prev = transcriptMeasuredRowsCache.get(item);
640
- if (!isStreamingAssistant && prev
594
+ if (prev
641
595
  && prev.rows === measured
642
596
  && prev.columns === frameColumns
643
597
  && prev.toolExpanded === toolExpandedFlag
@@ -650,19 +604,12 @@ export function useTranscriptWindow({
650
604
  toolExpanded: toolExpandedFlag,
651
605
  variantKey,
652
606
  });
653
- if (!isStreamingAssistant) {
654
- // First mount (no prior entry): this frame's row index already used the
655
- // ESTIMATE for this item (measuredTranscriptRows returned null). If Yoga
656
- // measured exactly what the estimate predicted, the geometry is already
657
- // correct — bumping measuredRowsVersion would re-run the row-index/window
658
- // memos for a no-op and repaint the card one frame later (the "settle"
659
- // jump). Only bump when the measurement actually corrects the estimate.
660
- // A CHANGED height (prev existed but differs) always bumps.
661
- const estimateRows = prev
662
- ? -1
663
- : estimateTranscriptItemRowsCached(item, frameColumns, toolOutputExpanded);
664
- if (prev || measured !== estimateRows) { changed = true; changedKeys.push(`${key}=${measured}`); }
665
- }
607
+ // First mount (no prior entry): this frame's row index already used the
608
+ // estimate. Only bump when Yoga actually corrects it.
609
+ const estimateRows = prev
610
+ ? -1
611
+ : estimateTranscriptItemRowsCached(item, frameColumns, toolOutputExpanded);
612
+ if (prev || measured !== estimateRows) { changed = true; changedKeys.push(`${key}=${measured}`); }
666
613
  }
667
614
  if (changed) {
668
615
  // `changed` only flips true when a height actually differs from the
@@ -723,7 +670,10 @@ export function useTranscriptWindow({
723
670
  const currentPosition = Math.max(0, Number(scrollPositionRef.current) || 0);
724
671
  const currentOffset = Math.max(0, Number(scrollOffset) || 0);
725
672
  const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
726
- const nearBottom = followingRef.current || currentTarget === 0;
673
+ // Follow ownership is explicit. target=0 can also mean the first manual
674
+ // read-back gesture is waiting for a newly mounted row measurement; never
675
+ // infer permission to chase the tail from the numeric offset alone.
676
+ const nearBottom = followingRef.current;
727
677
  const pinnedToBottom = nearBottom;
728
678
  // A genuine reading anchor must win over ordinary stream growth, but an
729
679
  // explicit follow arm (prompt submit / pinned bottom) must win over stale
@@ -88,14 +88,31 @@ const StableMarkdownChunk = React.memo(function StableMarkdownChunk({
88
88
  return <Markdown themeEpoch={themeEpoch} columns={columns}>{text}</Markdown>;
89
89
  });
90
90
 
91
- export function StreamingMarkdown({ children, themeEpoch = 0, columns, streamKey }) {
91
+ export function StreamingMarkdown({
92
+ children,
93
+ themeEpoch = 0,
94
+ columns,
95
+ streamKey,
96
+ streaming = true,
97
+ }) {
98
+ if (!streaming) {
99
+ return (
100
+ <Box flexDirection="column">
101
+ <Markdown themeEpoch={themeEpoch} columns={columns} trimPartialFences>{children}</Markdown>
102
+ </Box>
103
+ );
104
+ }
92
105
  const parts = resolveStreamingMarkdownParts(children, streamKey);
93
106
  if (parts.plain) {
94
107
  // Plain streaming text has no markdown tokens to style. Sending a growing
95
108
  // multi-line tail through renderTokenAnsiSegments/marked on every delta
96
109
  // reparses the whole response and dominates frame time; Ink can wrap the
97
110
  // identical visible text directly.
98
- return <Text color={theme.text} wrap="wrap">{parts.unstableForRender}</Text>;
111
+ return (
112
+ <Box flexDirection="column">
113
+ <Text color={theme.text} wrap="wrap">{parts.unstableForRender}</Text>
114
+ </Box>
115
+ );
99
116
  }
100
117
  const stableChunks = parts.stableChunks?.length
101
118
  ? parts.stableChunks
@@ -58,9 +58,14 @@ export const AssistantMessage = React.memo(function AssistantMessage({
58
58
  <Text color={theme.text}>{TURN_MARKER}</Text>
59
59
  </Box>
60
60
  <Box flexDirection="column" flexShrink={0} width={bodyWidth}>
61
- {streaming
62
- ? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth} streamKey={assistantId}>{renderText}</StreamingMarkdown>
63
- : <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
61
+ <StreamingMarkdown
62
+ themeEpoch={themeEpoch}
63
+ columns={bodyWidth}
64
+ streamKey={assistantId}
65
+ streaming={streaming}
66
+ >
67
+ {streaming ? renderText : text}
68
+ </StreamingMarkdown>
64
69
  </Box>
65
70
  </Box>
66
71
  );
@@ -368,17 +368,21 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
368
368
  const trailingColor = expandHintColor;
369
369
  let labelOut;
370
370
  let summaryOut;
371
+ // Shell headers stay label-only ("Ran 1 command") on every surface —
372
+ // opencode parity and the desktop card contract: the raw command line never
373
+ // rides inline in the header; it stays in the expanded detail body.
374
+ const headerSummaryText = isShellSurface ? '' : summaryText;
371
375
  if (stringWidth(labelText) >= avail) {
372
376
  labelOut = truncateToWidth(labelText, avail);
373
377
  summaryOut = '';
374
378
  } else {
375
379
  labelOut = labelText;
376
- const summaryBudget = avail - stringWidth(labelText) - (summaryText ? stringWidth(' ()') : 0);
380
+ const summaryBudget = avail - stringWidth(labelText) - (headerSummaryText ? stringWidth(' ()') : 0);
377
381
  // Cap by both the remaining header width and a fixed max so long
378
382
  // paths/queries get an ellipsis instead of dominating the line.
379
383
  const summaryWidth = Math.max(0, Math.min(summaryBudget, SUMMARY_MAX_CHARS));
380
- const truncatedSummary = summaryText && summaryWidth > 0
381
- ? truncateToWidth(summaryText, summaryWidth)
384
+ const truncatedSummary = headerSummaryText && summaryWidth > 0
385
+ ? truncateToWidth(headerSummaryText, summaryWidth)
382
386
  : '';
383
387
  summaryOut = truncatedSummary ? ` (${truncatedSummary})` : '';
384
388
  }