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.
- package/package.json +2 -1
- package/scripts/agent-model-liveness-test.mjs +68 -0
- package/scripts/anthropic-transport-policy-test.mjs +260 -0
- package/scripts/desktop-session-bridge-test.mjs +47 -0
- package/scripts/gemini-provider-test.mjs +396 -1
- package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
- package/scripts/interrupted-turn-history-test.mjs +28 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
- package/scripts/pending-completion-drop-test.mjs +160 -4
- package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
- package/scripts/process-lifecycle-test.mjs +18 -4
- package/scripts/prompt-input-parity-test.mjs +145 -0
- package/scripts/provider-admission-scheduler-test.mjs +4 -5
- package/scripts/provider-contract-test.mjs +91 -33
- package/scripts/provider-toolcall-test.mjs +97 -17
- package/scripts/streaming-tail-window-test.mjs +146 -0
- package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
- package/scripts/tui-runtime-load-bench.mjs +45 -0
- package/scripts/tui-store-frame-batch-test.mjs +99 -0
- package/scripts/tui-transcript-perf-test.mjs +367 -2
- package/scripts/write-backpressure-test.mjs +147 -0
- package/src/cli.mjs +5 -5
- package/src/lib/keychain-cjs.cjs +114 -25
- package/src/repl.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
- package/src/runtime/memory/lib/trace-store.mjs +66 -4
- package/src/runtime/shared/buffered-appender.mjs +83 -4
- package/src/runtime/shared/memory-snapshot.mjs +45 -36
- package/src/runtime/shared/process-lifecycle.mjs +166 -45
- package/src/runtime/shared/process-shutdown.mjs +2 -1
- package/src/session-runtime/model-route-api.mjs +4 -1
- package/src/session-runtime/native-search.mjs +4 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +8 -3
- package/src/session-runtime/resource-api.mjs +2 -1
- package/src/session-runtime/runtime-core.mjs +32 -0
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/warmup-schedulers.mjs +5 -1
- package/src/standalone/agent-tool.mjs +19 -1
- package/src/tui/App.jsx +10 -4
- package/src/tui/app/text-layout.mjs +9 -1
- package/src/tui/app/transcript-window.mjs +146 -60
- package/src/tui/app/use-mouse-input.mjs +16 -8
- package/src/tui/app/use-transcript-scroll.mjs +71 -19
- package/src/tui/app/use-transcript-window.mjs +21 -10
- package/src/tui/components/PromptInput.jsx +13 -6
- package/src/tui/dist/index.mjs +1094 -232
- package/src/tui/engine/context-state.mjs +31 -31
- package/src/tui/engine/frame-batched-store.mjs +75 -0
- package/src/tui/engine/session-api-ext.mjs +6 -1
- package/src/tui/engine/session-api.mjs +9 -3
- package/src/tui/engine/session-flow.mjs +54 -27
- package/src/tui/engine/turn.mjs +44 -3
- package/src/tui/engine.mjs +515 -36
- package/src/tui/input-editing.mjs +33 -13
- package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
- package/vendor/ink/build/ink.js +8 -16
|
@@ -82,7 +82,16 @@ const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)),
|
|
|
82
82
|
// cap and restore strictly-unbounded prep.
|
|
83
83
|
const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS', 120_000);
|
|
84
84
|
|
|
85
|
-
export function createStandaloneAgent({
|
|
85
|
+
export function createStandaloneAgent({
|
|
86
|
+
cfgMod,
|
|
87
|
+
reg,
|
|
88
|
+
mgr,
|
|
89
|
+
dataDir,
|
|
90
|
+
cwd: defaultCwd,
|
|
91
|
+
onSubagentEvent,
|
|
92
|
+
awaitKeychainPrewarm = async () => {},
|
|
93
|
+
isKeychainPrewarmReady = () => true,
|
|
94
|
+
}) {
|
|
86
95
|
// Optional bridge to the standard hook bus for SubagentStart / SubagentStop.
|
|
87
96
|
// Best-effort: a hook error must never affect worker spawn/finish.
|
|
88
97
|
function emitSubagentEvent(phase, agent, extra = {}) {
|
|
@@ -1588,6 +1597,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1588
1597
|
|
|
1589
1598
|
async function execute(args = {}, context = {}) {
|
|
1590
1599
|
try {
|
|
1600
|
+
await awaitKeychainPrewarm();
|
|
1591
1601
|
const type = clean(args.type) || 'spawn';
|
|
1592
1602
|
const callerCwd = clean(context.cwd || context.callerCwd);
|
|
1593
1603
|
const scopedContext = agentScope(args, context);
|
|
@@ -1731,6 +1741,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1731
1741
|
tools: [AGENT_TOOL],
|
|
1732
1742
|
execute,
|
|
1733
1743
|
getStatus: (context = {}) => {
|
|
1744
|
+
if (!isKeychainPrewarmReady()) {
|
|
1745
|
+
void awaitKeychainPrewarm();
|
|
1746
|
+
return { workers: [], jobs: [], scope: null };
|
|
1747
|
+
}
|
|
1734
1748
|
const scopedContext = agentScope({}, context);
|
|
1735
1749
|
const pid = terminalPidForContext(scopedContext);
|
|
1736
1750
|
return {
|
|
@@ -1740,6 +1754,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1740
1754
|
};
|
|
1741
1755
|
},
|
|
1742
1756
|
recoverWorkers: (context = {}) => {
|
|
1757
|
+
if (!isKeychainPrewarmReady()) {
|
|
1758
|
+
void awaitKeychainPrewarm();
|
|
1759
|
+
return [];
|
|
1760
|
+
}
|
|
1743
1761
|
const scopedContext = agentScope({ recover: true }, context);
|
|
1744
1762
|
refreshTagsFromSessions({ scanSessions: true, context: scopedContext });
|
|
1745
1763
|
return list({ scanSessions: false, context: scopedContext });
|
package/src/tui/App.jsx
CHANGED
|
@@ -2972,9 +2972,13 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2972
2972
|
overlayHintFallbackRow,
|
|
2973
2973
|
transcriptMeasureRef,
|
|
2974
2974
|
} = useTranscriptWindow({
|
|
2975
|
-
items: state.items,
|
|
2976
|
-
structureRevision: state.
|
|
2977
|
-
|
|
2975
|
+
items: state.transcriptViewItems || state.items,
|
|
2976
|
+
structureRevision: state.transcriptViewItems
|
|
2977
|
+
? state.transcriptViewRevision
|
|
2978
|
+
: state.structureRevision,
|
|
2979
|
+
// Historical pages are contiguous settled windows. Keep the independently
|
|
2980
|
+
// growing live tail hidden until paging forward reaches the live window.
|
|
2981
|
+
streamingTail: state.transcriptViewItems ? null : state.streamingTail,
|
|
2978
2982
|
themeEpoch: state.themeEpoch,
|
|
2979
2983
|
frameColumns,
|
|
2980
2984
|
toolOutputExpanded,
|
|
@@ -3167,7 +3171,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
3167
3171
|
const itemNode = (
|
|
3168
3172
|
<Item
|
|
3169
3173
|
item={item}
|
|
3170
|
-
prevKind={i > 0
|
|
3174
|
+
prevKind={i > 0
|
|
3175
|
+
? arr[i - 1].kind
|
|
3176
|
+
: (state.transcriptViewItems || state.items)[transcriptWindow.startIndex - 1]?.kind ?? null}
|
|
3171
3177
|
columns={frameColumns}
|
|
3172
3178
|
toolOutputExpanded={toolOutputExpanded}
|
|
3173
3179
|
rightMessage={attachOverlayHint ? inputHint : ''}
|
|
@@ -6,6 +6,7 @@ import { displayWidth } from '../display-width.mjs';
|
|
|
6
6
|
import wrapAnsi from 'wrap-ansi';
|
|
7
7
|
|
|
8
8
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
9
|
+
let promptRowsCache = null;
|
|
9
10
|
|
|
10
11
|
export function wrappedTextRows(value, width) {
|
|
11
12
|
const w = Math.max(1, Math.floor(Number(width) || 1));
|
|
@@ -43,7 +44,14 @@ export function promptContentRows(value, contentColumns) {
|
|
|
43
44
|
// offset, so reserve for the worst common case: caret at the end. This can
|
|
44
45
|
// over-reserve by one row at exact wrap boundaries, but never under-reserves
|
|
45
46
|
// and therefore prevents transcript rows from bleeding into the prompt box.
|
|
46
|
-
|
|
47
|
+
const text = String(value ?? '');
|
|
48
|
+
const width = Math.max(1, Math.floor(Number(contentColumns) || 1));
|
|
49
|
+
if (promptRowsCache?.text === text && promptRowsCache.width === width) {
|
|
50
|
+
return promptRowsCache.rows;
|
|
51
|
+
}
|
|
52
|
+
const rows = wrappedTextRows(`${text} `, width);
|
|
53
|
+
promptRowsCache = { text, width, rows };
|
|
54
|
+
return rows;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
// Rows an ink <Text wrap="wrap"> block occupies at `width`. Uses the SAME
|
|
@@ -435,7 +435,7 @@ export function lowerBound(values, target) {
|
|
|
435
435
|
let hi = values.length;
|
|
436
436
|
while (lo < hi) {
|
|
437
437
|
const mid = Math.floor((lo + hi) / 2);
|
|
438
|
-
if (values
|
|
438
|
+
if (transcriptRowAt(values, mid) < target) lo = mid + 1;
|
|
439
439
|
else hi = mid;
|
|
440
440
|
}
|
|
441
441
|
return lo;
|
|
@@ -446,7 +446,7 @@ export function upperBound(values, target) {
|
|
|
446
446
|
let hi = values.length;
|
|
447
447
|
while (lo < hi) {
|
|
448
448
|
const mid = Math.floor((lo + hi) / 2);
|
|
449
|
-
if (values
|
|
449
|
+
if (transcriptRowAt(values, mid) <= target) lo = mid + 1;
|
|
450
450
|
else hi = mid;
|
|
451
451
|
}
|
|
452
452
|
return lo;
|
|
@@ -457,16 +457,16 @@ export function upperBound(values, target) {
|
|
|
457
457
|
// render AND in the post-commit layout effect.
|
|
458
458
|
export function resolveAnchorScrollOffset({ anchor, items, curPrefix, totalRows, viewRows, maxRows }) {
|
|
459
459
|
if (!anchor || anchor.id == null) return null;
|
|
460
|
-
if (!
|
|
460
|
+
if (!curPrefix || curPrefix.length <= 1) return null;
|
|
461
461
|
const list = Array.isArray(items) ? items : [];
|
|
462
462
|
let idx = -1;
|
|
463
463
|
for (let i = list.length - 1; i >= 0; i--) {
|
|
464
464
|
if (list[i] && list[i].id === anchor.id) { idx = i; break; }
|
|
465
465
|
}
|
|
466
466
|
if (idx < 0 || idx > curPrefix.length - 2) return null;
|
|
467
|
-
const itemHeight = Math.max(0, (curPrefix
|
|
467
|
+
const itemHeight = Math.max(0, transcriptRowAt(curPrefix, idx + 1) - transcriptRowAt(curPrefix, idx));
|
|
468
468
|
const clampedOffset = Math.max(0, Math.min(Number(anchor.offset) || 0, itemHeight));
|
|
469
|
-
const anchorRowCur = (curPrefix
|
|
469
|
+
const anchorRowCur = transcriptRowAt(curPrefix, idx) + clampedOffset;
|
|
470
470
|
return Math.max(0, Math.min(maxRows, totalRows - viewRows - anchorRowCur));
|
|
471
471
|
}
|
|
472
472
|
|
|
@@ -585,12 +585,44 @@ export const streamingMeasuredRowsById = new Map();
|
|
|
585
585
|
// settle / invalidate delete sites in estimateTranscriptItemRowsCached.
|
|
586
586
|
const streamingEstimateHighWaterById = new Map();
|
|
587
587
|
|
|
588
|
+
// The App asks for the live-tail estimate on every render, including renders
|
|
589
|
+
// caused only by prompt typing. Keep exactly the latest estimate per stream id
|
|
590
|
+
// so unchanged text never reaches markdown/plain row measurement. The key
|
|
591
|
+
// includes every App-level input that selects this rendering path or geometry.
|
|
592
|
+
const streamingTailEstimateById = new Map();
|
|
593
|
+
const STREAMING_TAIL_ESTIMATE_LRU_MAX = 8;
|
|
594
|
+
|
|
595
|
+
function cacheStreamingTailEstimate(id, entry) {
|
|
596
|
+
if (id == null) return;
|
|
597
|
+
if (streamingTailEstimateById.has(id)) streamingTailEstimateById.delete(id);
|
|
598
|
+
streamingTailEstimateById.set(id, entry);
|
|
599
|
+
while (streamingTailEstimateById.size > STREAMING_TAIL_ESTIMATE_LRU_MAX) {
|
|
600
|
+
const oldest = streamingTailEstimateById.keys().next().value;
|
|
601
|
+
if (oldest === undefined) break;
|
|
602
|
+
streamingTailEstimateById.delete(oldest);
|
|
603
|
+
// The high-water entry belongs to the same latest-estimate lifecycle.
|
|
604
|
+
// Leaving it behind both grows without bound and lets a later reuse of an
|
|
605
|
+
// evicted id inherit geometry from an unrelated completed/aborted stream.
|
|
606
|
+
streamingEstimateHighWaterById.delete(oldest);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Lifecycle visibility for focused cache-boundary tests. */
|
|
611
|
+
export function streamingRowEstimateStateForId(id) {
|
|
612
|
+
return {
|
|
613
|
+
tailEstimate: streamingTailEstimateById.has(id),
|
|
614
|
+
highWater: streamingEstimateHighWaterById.has(id),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
588
618
|
/** True when either streaming id-keyed store holds entries, so the mount-prune
|
|
589
619
|
* call site fires even when only the estimate high-water map is populated (an
|
|
590
620
|
* item that never got a Yoga measurement but reached an estimate high-water,
|
|
591
621
|
* then was bulk-replaced, must still be pruned). */
|
|
592
622
|
export function hasStreamingRowStateToPrune() {
|
|
593
|
-
return streamingMeasuredRowsById.size > 0
|
|
623
|
+
return streamingMeasuredRowsById.size > 0
|
|
624
|
+
|| streamingEstimateHighWaterById.size > 0
|
|
625
|
+
|| streamingTailEstimateById.size > 0;
|
|
594
626
|
}
|
|
595
627
|
|
|
596
628
|
/** Drop streamingMeasuredRowsById entries for ids no longer mounted, so the
|
|
@@ -608,6 +640,11 @@ export function pruneStreamingMeasuredRowsById(liveIds) {
|
|
|
608
640
|
if (!liveIds.has(id)) streamingEstimateHighWaterById.delete(id);
|
|
609
641
|
}
|
|
610
642
|
}
|
|
643
|
+
if (streamingTailEstimateById.size > 0) {
|
|
644
|
+
for (const id of streamingTailEstimateById.keys()) {
|
|
645
|
+
if (!liveIds.has(id)) streamingTailEstimateById.delete(id);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
611
648
|
}
|
|
612
649
|
|
|
613
650
|
/** Copy a Yoga-measured row entry onto a replacement item object (e.g. patchItem). */
|
|
@@ -656,6 +693,21 @@ function assistantTextForStreamingRowEstimate(text) {
|
|
|
656
693
|
}
|
|
657
694
|
|
|
658
695
|
export function streamingEstimateRows(item, columns, toolOutputExpanded) {
|
|
696
|
+
const id = item?.id;
|
|
697
|
+
const exactText = String(item?.text ?? '');
|
|
698
|
+
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
699
|
+
const renderMode = item?.kind === 'assistant' && item?.streaming
|
|
700
|
+
? 'assistant-streaming-markdown'
|
|
701
|
+
: 'other';
|
|
702
|
+
const cached = id == null ? null : streamingTailEstimateById.get(id);
|
|
703
|
+
if (cached
|
|
704
|
+
&& cached.text === exactText
|
|
705
|
+
&& cached.columns === columns
|
|
706
|
+
&& cached.toolExpanded === toolExpanded
|
|
707
|
+
&& cached.renderMode === renderMode) {
|
|
708
|
+
cacheStreamingTailEstimate(id, cached);
|
|
709
|
+
return cached.rows;
|
|
710
|
+
}
|
|
659
711
|
const trimmedText = assistantTextForStreamingRowEstimate(item.text);
|
|
660
712
|
const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
|
|
661
713
|
const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
|
|
@@ -664,16 +716,32 @@ export function streamingEstimateRows(item, columns, toolOutputExpanded) {
|
|
|
664
716
|
// this id already reached this run — absorbs the childCount 1↔2 gap flip that
|
|
665
717
|
// otherwise dips the estimate ±1 frame-to-frame. A columns/expanded change
|
|
666
718
|
// resets the water line (new width → legitimately new row count).
|
|
667
|
-
const id = item?.id;
|
|
668
719
|
if (id == null) return quantized;
|
|
669
|
-
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
670
720
|
const prev = streamingEstimateHighWaterById.get(id);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
721
|
+
let rows;
|
|
722
|
+
if (!prev
|
|
723
|
+
|| prev.columns !== columns
|
|
724
|
+
|| prev.toolExpanded !== toolExpanded
|
|
725
|
+
|| prev.renderMode !== renderMode) {
|
|
726
|
+
streamingEstimateHighWaterById.set(id, {
|
|
727
|
+
rows: quantized,
|
|
728
|
+
columns,
|
|
729
|
+
toolExpanded,
|
|
730
|
+
renderMode,
|
|
731
|
+
});
|
|
732
|
+
rows = quantized;
|
|
733
|
+
} else {
|
|
734
|
+
if (quantized > prev.rows) prev.rows = quantized;
|
|
735
|
+
rows = prev.rows;
|
|
674
736
|
}
|
|
675
|
-
|
|
676
|
-
|
|
737
|
+
cacheStreamingTailEstimate(id, {
|
|
738
|
+
text: exactText,
|
|
739
|
+
columns,
|
|
740
|
+
toolExpanded,
|
|
741
|
+
renderMode,
|
|
742
|
+
rows,
|
|
743
|
+
});
|
|
744
|
+
return rows;
|
|
677
745
|
}
|
|
678
746
|
|
|
679
747
|
// ── Same-frame streaming-tail growth compensation (scrolled-up only) ────────
|
|
@@ -754,6 +822,7 @@ export function estimateTranscriptItemRowsCached(item, columns, toolOutputExpand
|
|
|
754
822
|
if (idEntry) {
|
|
755
823
|
streamingMeasuredRowsById.delete(item.id);
|
|
756
824
|
streamingEstimateHighWaterById.delete(item.id);
|
|
825
|
+
streamingTailEstimateById.delete(item.id);
|
|
757
826
|
}
|
|
758
827
|
// No confirmed measurement yet for this id (item just started streaming):
|
|
759
828
|
// nothing to defer against, so the first frame falls back to the estimate.
|
|
@@ -765,6 +834,7 @@ export function estimateTranscriptItemRowsCached(item, columns, toolOutputExpand
|
|
|
765
834
|
// owns its height. Clear both so a later id reuse / this run cannot leak.
|
|
766
835
|
if (streamingMeasuredRowsById.has(item.id)) streamingMeasuredRowsById.delete(item.id);
|
|
767
836
|
if (streamingEstimateHighWaterById.has(item.id)) streamingEstimateHighWaterById.delete(item.id);
|
|
837
|
+
if (streamingTailEstimateById.has(item.id)) streamingTailEstimateById.delete(item.id);
|
|
768
838
|
}
|
|
769
839
|
const variantKey = transcriptItemVariantKey(item);
|
|
770
840
|
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
@@ -786,13 +856,16 @@ export function buildTranscriptRowIndex(items, {
|
|
|
786
856
|
columns = 80,
|
|
787
857
|
toolOutputExpanded = false,
|
|
788
858
|
suppressMeasuredRowHeights = false,
|
|
859
|
+
streamingTailItem = null,
|
|
789
860
|
} = {}) {
|
|
790
861
|
const allItems = Array.isArray(items) ? items : [];
|
|
791
862
|
const rows = new Array(allItems.length);
|
|
792
863
|
const prefixRows = new Array(allItems.length + 1);
|
|
793
864
|
prefixRows[0] = 0;
|
|
794
865
|
for (let i = 0; i < allItems.length; i++) {
|
|
795
|
-
const item = allItems
|
|
866
|
+
const item = streamingTailItem && i === allItems.length - 1
|
|
867
|
+
? streamingTailItem
|
|
868
|
+
: allItems[i];
|
|
796
869
|
const measured = suppressMeasuredRowHeights
|
|
797
870
|
? null
|
|
798
871
|
: measuredTranscriptRows(item, columns, toolOutputExpanded);
|
|
@@ -833,9 +906,10 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
833
906
|
measuredRowsVersion = 0,
|
|
834
907
|
cacheRef = null,
|
|
835
908
|
prefixRevision = null,
|
|
909
|
+
streamingTailItem = null,
|
|
836
910
|
} = {}) {
|
|
837
911
|
const allItems = Array.isArray(items) ? items : [];
|
|
838
|
-
const tail = trailingStreamingItem(allItems);
|
|
912
|
+
const tail = streamingTailItem || trailingStreamingItem(allItems);
|
|
839
913
|
// Per-hook-instance cache holder (useRef object). Fall back to a throwaway
|
|
840
914
|
// holder if none supplied so the builder still works in isolation.
|
|
841
915
|
const holder = cacheRef || { current: null };
|
|
@@ -845,7 +919,7 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
845
919
|
if (!tail) {
|
|
846
920
|
holder.current = null;
|
|
847
921
|
return buildTranscriptRowIndex(allItems, {
|
|
848
|
-
columns, toolOutputExpanded, suppressMeasuredRowHeights,
|
|
922
|
+
columns, toolOutputExpanded, suppressMeasuredRowHeights, streamingTailItem,
|
|
849
923
|
});
|
|
850
924
|
}
|
|
851
925
|
const prefixLen = allItems.length - 1;
|
|
@@ -873,11 +947,14 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
873
947
|
const tailRows = tailMeasured != null
|
|
874
948
|
? tailMeasured
|
|
875
949
|
: estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
950
|
+
// Immutable segmented views append one virtual tail slot to the cached
|
|
951
|
+
// settled prefix. No committed array is mutated and no prefix is copied.
|
|
952
|
+
const totalRows = cache.prefixTotal + tailRows;
|
|
953
|
+
return {
|
|
954
|
+
rows: appendTranscriptRow(cache.prefixRowsArr, tailRows),
|
|
955
|
+
prefixRows: appendTranscriptRow(cache.prefixPrefixRows, totalRows),
|
|
956
|
+
totalRows,
|
|
957
|
+
};
|
|
881
958
|
}
|
|
882
959
|
}
|
|
883
960
|
// Cache miss: full build, then repopulate the settled-prefix cache so the NEXT
|
|
@@ -885,7 +962,7 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
885
962
|
// full-build outputs truncated before the tail row — byte-identical to a
|
|
886
963
|
// full rebuild's prefix by construction.
|
|
887
964
|
const full = buildTranscriptRowIndex(allItems, {
|
|
888
|
-
columns, toolOutputExpanded, suppressMeasuredRowHeights,
|
|
965
|
+
columns, toolOutputExpanded, suppressMeasuredRowHeights, streamingTailItem,
|
|
889
966
|
});
|
|
890
967
|
holder.current = {
|
|
891
968
|
columns,
|
|
@@ -902,46 +979,55 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
902
979
|
return full;
|
|
903
980
|
}
|
|
904
981
|
|
|
905
|
-
// Stable signature for
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
export function transcriptStructureSignature(items, columns, toolOutputExpanded) {
|
|
982
|
+
// Stable O(1) signature for transcript row-index/window memos. The engine's
|
|
983
|
+
// monotonic prefixRevision proves settled-prefix identity; only the live tail
|
|
984
|
+
// needs resolving at render time.
|
|
985
|
+
export function transcriptStructureSignature(items, columns, toolOutputExpanded, prefixRevision = 0, streamingTailItem = null) {
|
|
911
986
|
const list = Array.isArray(items) ? items : [];
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
987
|
+
const tail = streamingTailItem || trailingStreamingItem(list);
|
|
988
|
+
const tailRows = tail
|
|
989
|
+
? estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded)
|
|
990
|
+
: 0;
|
|
991
|
+
return `${Math.max(0, Number(prefixRevision) || 0)}|${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}|${tail?.id ?? '_'}:${tailRows}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export function transcriptRowAt(values, index) {
|
|
995
|
+
if (!values || index < 0 || index >= values.length) return 0;
|
|
996
|
+
return typeof values.atIndex === 'function'
|
|
997
|
+
? values.atIndex(index)
|
|
998
|
+
: (Number(values[index]) || 0);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function appendTranscriptRow(prefix, tailValue) {
|
|
1002
|
+
return Object.freeze({
|
|
1003
|
+
length: prefix.length + 1,
|
|
1004
|
+
atIndex: (index) => index === prefix.length
|
|
1005
|
+
? tailValue
|
|
1006
|
+
: (Number(prefix[index]) || 0),
|
|
1007
|
+
slice: (start = 0, end = prefix.length + 1) => {
|
|
1008
|
+
const values = [];
|
|
1009
|
+
const lo = Math.max(0, start < 0 ? prefix.length + 1 + start : start);
|
|
1010
|
+
const hi = Math.min(prefix.length + 1, end < 0 ? prefix.length + 1 + end : end);
|
|
1011
|
+
for (let index = lo; index < hi; index++) {
|
|
1012
|
+
values.push(index === prefix.length ? tailValue : prefix[index]);
|
|
937
1013
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1014
|
+
return values;
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export function transcriptItemsWithStableTail(settledItems, streamingTailItem, cacheRef) {
|
|
1020
|
+
const settled = Array.isArray(settledItems) ? settledItems : [];
|
|
1021
|
+
if (!streamingTailItem) return settled;
|
|
1022
|
+
const previous = cacheRef?.current;
|
|
1023
|
+
if (previous
|
|
1024
|
+
&& previous.settled === settled
|
|
1025
|
+
&& previous.tailId === streamingTailItem.id) {
|
|
1026
|
+
return previous.items;
|
|
943
1027
|
}
|
|
944
|
-
|
|
1028
|
+
const items = [...settled, streamingTailItem];
|
|
1029
|
+
if (cacheRef) cacheRef.current = { settled, tailId: streamingTailItem.id, items };
|
|
1030
|
+
return items;
|
|
945
1031
|
}
|
|
946
1032
|
|
|
947
1033
|
export function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24, columns = 80, toolOutputExpanded = false, rowIndex = null } = {}) {
|
|
@@ -995,7 +1081,7 @@ export function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight
|
|
|
995
1081
|
if (endIndex - startIndex > effectiveMaxItems) startIndex = Math.max(0, endIndex - effectiveMaxItems);
|
|
996
1082
|
}
|
|
997
1083
|
|
|
998
|
-
const bottomSpacerRows = Math.max(0, totalRows - (prefixRows
|
|
1084
|
+
const bottomSpacerRows = Math.max(0, totalRows - (transcriptRowAt(prefixRows, endIndex) || totalRows));
|
|
999
1085
|
return {
|
|
1000
1086
|
startIndex,
|
|
1001
1087
|
endIndex,
|
|
@@ -199,6 +199,11 @@ export function useMouseInput({
|
|
|
199
199
|
promptMouseSelectionRef.current?.clear?.();
|
|
200
200
|
applySelectionRect(null);
|
|
201
201
|
};
|
|
202
|
+
// Windows Terminal's native selection overlay survives incremental frames.
|
|
203
|
+
// Clear it only when an app-owned gesture completes, never on drag motion.
|
|
204
|
+
const finishWindowsMouseGesture = () => {
|
|
205
|
+
if (IS_WINDOWS_TERMINAL) store.forceRenderRepaint?.();
|
|
206
|
+
};
|
|
202
207
|
// Edge auto-scroll TIMER (ref: ScrollKeybindingHandler useDragToScroll).
|
|
203
208
|
// SGR mode 1002 reports drag-motion only when the pointer changes CELL, so
|
|
204
209
|
// a pointer held stationary at the top/bottom edge stops emitting events
|
|
@@ -304,6 +309,7 @@ export function useMouseInput({
|
|
|
304
309
|
if (dragRef.current.active) {
|
|
305
310
|
const last = dragRef.current.last || {};
|
|
306
311
|
finalizeActiveDrag(Number(last.x) || 0, Number(last.y) || 0);
|
|
312
|
+
finishWindowsMouseGesture();
|
|
307
313
|
} else {
|
|
308
314
|
stopEdgeAutoscroll();
|
|
309
315
|
}
|
|
@@ -352,14 +358,8 @@ export function useMouseInput({
|
|
|
352
358
|
// never forwards those events; keep this guard as a belt-and-braces so
|
|
353
359
|
// a future WT that starts forwarding them can never double-paint.
|
|
354
360
|
if (IS_WINDOWS_TERMINAL && shiftHeld) return;
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// second highlight. Any app-owned button press means the user is done
|
|
358
|
-
// with that native selection: force one full clear+rewrite frame
|
|
359
|
-
// (BSU/ESU-wrapped, no flash) which dismisses it before we paint ours.
|
|
360
|
-
if (IS_WINDOWS_TERMINAL && press && !isMotion) {
|
|
361
|
-
store.forceRenderRepaint?.();
|
|
362
|
-
}
|
|
361
|
+
// Do not force a full clear/rewrite on app-owned presses. Selection
|
|
362
|
+
// changes below repaint through Ink's normal maxFps render throttle.
|
|
363
363
|
// Ctrl+left-click is the app-side extend trigger (baseButton =
|
|
364
364
|
// button & 3 already maps ctrl+left press to button 0); right-button
|
|
365
365
|
// press is the second trigger (its own block below). This is
|
|
@@ -384,6 +384,7 @@ export function useMouseInput({
|
|
|
384
384
|
dragRef.current = { anchor: { x, y }, anchorScroll: 0, last: { x, y }, active: false, rect: null, region: 'prompt', anchorSpan: null };
|
|
385
385
|
if (offset != null) ctl.extendTo?.(offset, true);
|
|
386
386
|
lastClickRef.current = { x: -1, y: -1, t: 0 };
|
|
387
|
+
finishWindowsMouseGesture();
|
|
387
388
|
}
|
|
388
389
|
return;
|
|
389
390
|
}
|
|
@@ -401,6 +402,7 @@ export function useMouseInput({
|
|
|
401
402
|
dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
|
|
402
403
|
applySelectionRect(rect);
|
|
403
404
|
lastClickRef.current = { x, y, t: nowR, count: 1 };
|
|
405
|
+
finishWindowsMouseGesture();
|
|
404
406
|
return;
|
|
405
407
|
}
|
|
406
408
|
if (
|
|
@@ -420,6 +422,7 @@ export function useMouseInput({
|
|
|
420
422
|
dragRef.current = { ...dragRef.current, last: { x, y: selectionY }, active: false, region: regionR };
|
|
421
423
|
applySelectionRect(rect);
|
|
422
424
|
lastClickRef.current = { x, y, t: nowR, count: 1 };
|
|
425
|
+
finishWindowsMouseGesture();
|
|
423
426
|
return;
|
|
424
427
|
}
|
|
425
428
|
return;
|
|
@@ -486,6 +489,7 @@ export function useMouseInput({
|
|
|
486
489
|
dragRef.current.region = null;
|
|
487
490
|
dragRef.current.anchorSpan = null;
|
|
488
491
|
clearAllSelections();
|
|
492
|
+
finishWindowsMouseGesture();
|
|
489
493
|
return;
|
|
490
494
|
}
|
|
491
495
|
const region = inTranscript ? 'transcript' : 'status';
|
|
@@ -671,6 +675,10 @@ export function useMouseInput({
|
|
|
671
675
|
// path the ctrl+wheel zoom passthrough uses. Guarded to baseButton 0
|
|
672
676
|
// so a stray right-button release never finalizes.
|
|
673
677
|
finalizeActiveDrag(x, y);
|
|
678
|
+
// WT keeps its native shift-selection overlay across incremental
|
|
679
|
+
// frames. Dismiss it once per completed gesture, after publishing the
|
|
680
|
+
// final app selection, rather than forcing a full rewrite on press.
|
|
681
|
+
finishWindowsMouseGesture();
|
|
674
682
|
}
|
|
675
683
|
}
|
|
676
684
|
};
|