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
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
applyCompatProviderChatOptions,
|
|
13
13
|
parseToolCalls,
|
|
14
14
|
} from '../src/runtime/agent/orchestrator/providers/openai-compat.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
consumeCompatChatCompletionStream,
|
|
17
|
+
consumeCompatResponsesStream,
|
|
18
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
19
|
+
import { useXaiResponsesWebSocket } from '../src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs';
|
|
20
|
+
import { classifyError } from '../src/runtime/agent/orchestrator/providers/retry-classifier.mjs';
|
|
16
21
|
import { GrokOAuthProvider } from '../src/runtime/agent/orchestrator/providers/grok-oauth.mjs';
|
|
17
22
|
import { sendViaWebSocket } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
18
23
|
import {
|
|
@@ -341,13 +346,18 @@ test('Grok OAuth end-to-end HTTP Responses path is hermetic through inner compat
|
|
|
341
346
|
},
|
|
342
347
|
responsesTransport: 'http',
|
|
343
348
|
});
|
|
344
|
-
provider.ensureAuth = async () => ({ access_token: 'fixture-token' });
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
provider.ensureAuth = async () => ({ access_token: 'fixture-token', user_id: 'fixture-user' });
|
|
350
|
+
const ensureInner = provider._ensureInner.bind(provider);
|
|
351
|
+
let capturedInner = null;
|
|
352
|
+
let capturedParams = null;
|
|
353
|
+
provider._ensureInner = (...args) => {
|
|
354
|
+
const inner = ensureInner(...args);
|
|
355
|
+
capturedInner = inner;
|
|
356
|
+
inner.client = {
|
|
357
|
+
responses: {
|
|
358
|
+
create: async (params) => {
|
|
359
|
+
capturedParams = params;
|
|
360
|
+
return stream([
|
|
351
361
|
{ type: 'response.created', response: { id: 'resp_fixture', model: 'grok-4.5' } },
|
|
352
362
|
{ type: 'response.output_text.delta', delta: 'hermetic' },
|
|
353
363
|
{
|
|
@@ -360,20 +370,66 @@ test('Grok OAuth end-to-end HTTP Responses path is hermetic through inner compat
|
|
|
360
370
|
usage: { input_tokens: 3, output_tokens: 1 },
|
|
361
371
|
},
|
|
362
372
|
},
|
|
363
|
-
|
|
364
|
-
|
|
373
|
+
]);
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
return inner;
|
|
365
378
|
};
|
|
366
379
|
|
|
367
380
|
const result = await provider.send(
|
|
368
381
|
[{ role: 'user', content: 'fixture' }],
|
|
369
382
|
'grok-4.5',
|
|
370
383
|
[],
|
|
371
|
-
{ sessionId: 'hermetic-grok-contract' },
|
|
384
|
+
{ sessionId: 'hermetic-grok-contract', iteration: 2 },
|
|
372
385
|
);
|
|
373
386
|
assert.equal(result.content, 'hermetic');
|
|
374
387
|
assert.equal(result.usage.inputTokens, 3);
|
|
388
|
+
assert.equal(capturedInner.config.preconnect, false, 'Grok seam must propagate to inner compat');
|
|
389
|
+
assert.equal(typeof capturedInner.config.preconnectFn, 'function');
|
|
390
|
+
assert.equal(capturedInner.baseURL, 'https://cli-chat-proxy.grok.com/v1');
|
|
391
|
+
assert.equal(capturedInner.config.responsesTransport, 'http');
|
|
392
|
+
assert.equal(capturedInner.defaultHeaders['x-grok-conv-id'], undefined);
|
|
393
|
+
assert.equal(capturedInner.defaultHeaders['x-grok-session-id'], 'hermetic-grok-contract');
|
|
394
|
+
assert.match(capturedInner.defaultHeaders['x-grok-req-id'], /^[0-9a-f-]{36}$/);
|
|
395
|
+
assert.equal(capturedInner.defaultHeaders['x-grok-model-override'], 'grok-4.5');
|
|
396
|
+
assert.equal(capturedInner.defaultHeaders['x-grok-turn-idx'], '2');
|
|
397
|
+
assert.equal(capturedInner.defaultHeaders['x-grok-user-id'], 'fixture-user');
|
|
398
|
+
assert.equal(capturedParams.store, false);
|
|
399
|
+
assert.deepEqual(capturedParams.include, ['reasoning.encrypted_content']);
|
|
400
|
+
assert.equal(result.providerState.xaiResponses.store, false);
|
|
401
|
+
assert.equal(result.providerState.xaiResponses.previousResponseId, null);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('xAI Responses defaults to HTTP and keeps WebSocket behind explicit opt-in', () => {
|
|
405
|
+
assert.equal(useXaiResponsesWebSocket({}, {}), false);
|
|
406
|
+
assert.equal(useXaiResponsesWebSocket({}, { responsesTransport: 'http' }), false);
|
|
407
|
+
assert.equal(useXaiResponsesWebSocket({}, { responsesTransport: 'websocket' }), true);
|
|
375
408
|
});
|
|
376
409
|
|
|
410
|
+
for (const event of [
|
|
411
|
+
{ type: 'response.failed', response: { error: { message: 'forbidden' } } },
|
|
412
|
+
{ type: 'error', message: 'forbidden' },
|
|
413
|
+
]) {
|
|
414
|
+
test(`xAI ${event.type} stream event is retryable 500-equivalent without changing other compat labels`, async () => {
|
|
415
|
+
const xaiError = await consumeCompatResponsesStream(stream([event]), {
|
|
416
|
+
label: 'xai:responses',
|
|
417
|
+
parseResponsesToolCalls: () => [],
|
|
418
|
+
responseOutputText: () => '',
|
|
419
|
+
}).then(() => null, (err) => err);
|
|
420
|
+
assert.equal(xaiError.httpStatus, 500);
|
|
421
|
+
assert.equal(classifyError(xaiError), 'transient');
|
|
422
|
+
|
|
423
|
+
const otherError = await consumeCompatResponsesStream(stream([event]), {
|
|
424
|
+
label: 'other-compat',
|
|
425
|
+
parseResponsesToolCalls: () => [],
|
|
426
|
+
responseOutputText: () => '',
|
|
427
|
+
}).then(() => null, (err) => err);
|
|
428
|
+
assert.equal(otherError.httpStatus, 403);
|
|
429
|
+
assert.equal(classifyError(otherError), 'auth');
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
377
433
|
test('Grok OAuth does not refresh/replay a 401 after visible tool dispatch', async () => {
|
|
378
434
|
const provider = Object.create(GrokOAuthProvider.prototype);
|
|
379
435
|
provider.config = { preconnect: false };
|
|
@@ -500,26 +556,28 @@ test('xAI safe 401 replay carries completed warmup into the retry', async () =>
|
|
|
500
556
|
assert.equal(attempts, 2);
|
|
501
557
|
});
|
|
502
558
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
559
|
+
for (const status of [401, 403]) {
|
|
560
|
+
test(`Grok OAuth safe ${status} replay carries completed warmup to refreshed xAI inner`, async () => {
|
|
561
|
+
const provider = Object.create(GrokOAuthProvider.prototype);
|
|
562
|
+
provider.config = { preconnect: false };
|
|
563
|
+
const warmup = { usage: { inputTokens: 10 } };
|
|
564
|
+
let authCalls = 0;
|
|
565
|
+
provider.ensureAuth = async ({ forceRefresh = false } = {}) => {
|
|
566
|
+
authCalls += 1;
|
|
567
|
+
return { access_token: forceRefresh ? 'fresh' : 'stale' };
|
|
568
|
+
};
|
|
569
|
+
provider._ensureInner = (token) => ({
|
|
570
|
+
_doSend: async (_messages, _model, _tools, opts) => {
|
|
571
|
+
if (token === 'stale') {
|
|
572
|
+
const err = Object.assign(new Error(String(status)), { httpStatus: status });
|
|
573
|
+
Object.defineProperty(err, '__warmup', { value: warmup });
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
assert.equal(opts._carriedWarmup, warmup);
|
|
577
|
+
return { content: 'retried' };
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
assert.equal((await provider.send([], 'grok-4.5', [], {})).content, 'retried');
|
|
581
|
+
assert.equal(authCalls, 2);
|
|
522
582
|
});
|
|
523
|
-
|
|
524
|
-
assert.equal(authCalls, 2);
|
|
525
|
-
});
|
|
583
|
+
}
|
|
@@ -833,6 +833,89 @@ test('openai-compat/xai Responses: load_tool history replays as function_call',
|
|
|
833
833
|
assert.equal(input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
|
|
834
834
|
});
|
|
835
835
|
|
|
836
|
+
test('openai-compat/xai store=false continuation replays the full conversation with encrypted reasoning', () => {
|
|
837
|
+
const encrypted = {
|
|
838
|
+
type: 'reasoning',
|
|
839
|
+
id: 'reasoning_1',
|
|
840
|
+
encrypted_content: 'opaque-ciphertext',
|
|
841
|
+
summary: [],
|
|
842
|
+
};
|
|
843
|
+
const { input, previousResponseId, startIndex } = _toXaiResponsesInputForTest([
|
|
844
|
+
{ role: 'user', content: 'call read' },
|
|
845
|
+
{
|
|
846
|
+
role: 'assistant',
|
|
847
|
+
content: '',
|
|
848
|
+
toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }],
|
|
849
|
+
},
|
|
850
|
+
{ role: 'tool', toolCallId: 'call_1', content: 'contents' },
|
|
851
|
+
], {
|
|
852
|
+
xaiResponses: {
|
|
853
|
+
previousResponseId: null,
|
|
854
|
+
responseId: 'resp_unstored',
|
|
855
|
+
store: false,
|
|
856
|
+
encryptedReasoningItems: [encrypted],
|
|
857
|
+
seenMessageCount: 1,
|
|
858
|
+
model: 'grok-4.5',
|
|
859
|
+
},
|
|
860
|
+
}, { model: 'grok-4.5' });
|
|
861
|
+
assert.equal(previousResponseId, null);
|
|
862
|
+
assert.equal(startIndex, 0);
|
|
863
|
+
assert.equal(input[0].role, 'user');
|
|
864
|
+
assert.deepEqual(input[1], encrypted);
|
|
865
|
+
assert.equal(input[2].call_id, 'call_1');
|
|
866
|
+
assert.equal(input[3].call_id, 'call_1');
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test('openai-compat/xai store=false second tool round retains original system/user and both tool results', () => {
|
|
870
|
+
const firstReasoning = {
|
|
871
|
+
type: 'reasoning',
|
|
872
|
+
id: 'reasoning_1',
|
|
873
|
+
encrypted_content: 'opaque-first',
|
|
874
|
+
summary: [],
|
|
875
|
+
};
|
|
876
|
+
const secondReasoning = {
|
|
877
|
+
type: 'reasoning',
|
|
878
|
+
id: 'reasoning_2',
|
|
879
|
+
encrypted_content: 'opaque-second',
|
|
880
|
+
summary: [],
|
|
881
|
+
};
|
|
882
|
+
const { input, previousResponseId, startIndex } = _toXaiResponsesInputForTest([
|
|
883
|
+
{ role: 'system', content: 'original system' },
|
|
884
|
+
{ role: 'user', content: 'original user request' },
|
|
885
|
+
{ role: 'assistant', content: '', toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }] },
|
|
886
|
+
{ role: 'tool', toolCallId: 'call_1', content: 'first tool result' },
|
|
887
|
+
{ role: 'assistant', content: '', toolCalls: [{ id: 'call_2', name: 'grep', arguments: { pattern: 'x' } }] },
|
|
888
|
+
{ role: 'tool', toolCallId: 'call_2', content: 'second tool result' },
|
|
889
|
+
], {
|
|
890
|
+
xaiResponses: {
|
|
891
|
+
previousResponseId: null,
|
|
892
|
+
responseId: 'resp_unstored_2',
|
|
893
|
+
store: false,
|
|
894
|
+
encryptedReasoningHistory: [
|
|
895
|
+
{ messageIndex: 2, items: [firstReasoning] },
|
|
896
|
+
{ messageIndex: 4, items: [secondReasoning] },
|
|
897
|
+
],
|
|
898
|
+
seenMessageCount: 4,
|
|
899
|
+
model: 'grok-4.5',
|
|
900
|
+
},
|
|
901
|
+
}, { model: 'grok-4.5' });
|
|
902
|
+
assert.equal(previousResponseId, null);
|
|
903
|
+
assert.equal(startIndex, 0);
|
|
904
|
+
assert.equal(input[0].role, 'system');
|
|
905
|
+
assert.equal(input[0].content[0].text, 'original system');
|
|
906
|
+
assert.equal(input[1].role, 'user');
|
|
907
|
+
assert.equal(input[1].content[0].text, 'original user request');
|
|
908
|
+
assert.deepEqual(input.filter((item) => item.type === 'reasoning'), [firstReasoning, secondReasoning]);
|
|
909
|
+
assert.deepEqual(
|
|
910
|
+
input.filter((item) => item.type === 'function_call').map((item) => item.call_id),
|
|
911
|
+
['call_1', 'call_2'],
|
|
912
|
+
);
|
|
913
|
+
assert.deepEqual(
|
|
914
|
+
input.filter((item) => item.type === 'function_call_output').map((item) => item.output),
|
|
915
|
+
['first tool result', 'second tool result'],
|
|
916
|
+
);
|
|
917
|
+
});
|
|
918
|
+
|
|
836
919
|
test('openai-compat/xai Responses: model switch drops prior tool transcript history', () => {
|
|
837
920
|
const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
|
|
838
921
|
{ role: 'system', content: 'sys' },
|
|
@@ -2869,7 +2952,7 @@ test('openai-compat/xai: HTTP pin uses only the injected preconnect seam', async
|
|
|
2869
2952
|
}
|
|
2870
2953
|
});
|
|
2871
2954
|
|
|
2872
|
-
test('grok-oauth:
|
|
2955
|
+
test('grok-oauth: every OAuth model is pinned to the CLI proxy over HTTP/SSE', () => {
|
|
2873
2956
|
const prevOaiTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2874
2957
|
const prevResponsesTransport = process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT;
|
|
2875
2958
|
const prevGrokTransport = process.env.MIXDOG_GROK_OAUTH_TRANSPORT;
|
|
@@ -2879,22 +2962,19 @@ test('grok-oauth: all Grok models inherit global transport; explicit setting is
|
|
|
2879
2962
|
delete process.env.MIXDOG_GROK_OAUTH_TRANSPORT;
|
|
2880
2963
|
const provider = new GrokOAuthProvider({});
|
|
2881
2964
|
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
assert.equal(pinnedProxy.config.responsesTransport, 'http');
|
|
2896
|
-
const pinnedApi = pinnedProvider._ensureInner('tok2', 'grok-build-0.1');
|
|
2897
|
-
assert.equal(pinnedApi.config.responsesTransport, 'http');
|
|
2965
|
+
for (const model of ['grok-build-0.1', 'grok-build', 'grok-4.5']) {
|
|
2966
|
+
const inner = provider._ensureInner(`tok-${model}`, model);
|
|
2967
|
+
assert.equal(inner.config.responsesTransport, 'http');
|
|
2968
|
+
assert.equal(inner.baseURL, 'https://cli-chat-proxy.grok.com/v1');
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// OAuth routing is a security boundary: even explicit WS settings
|
|
2972
|
+
// cannot send the session bearer to the fixed api.x.ai WS endpoint.
|
|
2973
|
+
process.env.MIXDOG_GROK_OAUTH_RESPONSES_TRANSPORT = 'websocket';
|
|
2974
|
+
const pinnedProvider = new GrokOAuthProvider({ responsesTransport: 'websocket' });
|
|
2975
|
+
const pinned = pinnedProvider._ensureInner('tok-explicit', 'grok-4.5');
|
|
2976
|
+
assert.equal(pinned.config.responsesTransport, 'http');
|
|
2977
|
+
assert.equal(pinned.baseURL, 'https://cli-chat-proxy.grok.com/v1');
|
|
2898
2978
|
} finally {
|
|
2899
2979
|
if (prevOaiTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2900
2980
|
else process.env.MIXDOG_OAI_TRANSPORT = prevOaiTransport;
|
|
@@ -6,7 +6,21 @@ import {
|
|
|
6
6
|
} from '../src/tui/markdown/streaming-markdown.mjs';
|
|
7
7
|
import {
|
|
8
8
|
measureStreamingMarkdownRenderedRows,
|
|
9
|
+
measureStreamingMarkdownRenderedRowsUncached,
|
|
9
10
|
} from '../src/tui/markdown/measure-rendered-rows.mjs';
|
|
11
|
+
import {
|
|
12
|
+
buildTranscriptRowIndex,
|
|
13
|
+
streamingRowEstimateStateForId,
|
|
14
|
+
streamingEstimateRows,
|
|
15
|
+
transcriptRenderWindow,
|
|
16
|
+
} from '../src/tui/app/transcript-window.mjs';
|
|
17
|
+
|
|
18
|
+
function assertCachedEqualsDirect(text, columns, key) {
|
|
19
|
+
const cached = measureStreamingMarkdownRenderedRows(text, columns, key);
|
|
20
|
+
const direct = measureStreamingMarkdownRenderedRowsUncached(text, columns, key);
|
|
21
|
+
assert.equal(cached, direct);
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
test('bottom-pinned plain streaming text keeps only the visible suffix', () => {
|
|
12
26
|
const lines = Array.from({ length: 100 }, (_, index) => `plain line ${index}`);
|
|
@@ -27,3 +41,135 @@ test('wrapped plain lines consume the streaming row budget', () => {
|
|
|
27
41
|
assert.equal(windowPlainStreamingText(lines.join('\n'), 80, 2), lines.slice(-1).join('\n'));
|
|
28
42
|
assert.equal(windowPlainStreamingText(lines.join('\n'), 80, 3), lines.slice(-2).join('\n'));
|
|
29
43
|
});
|
|
44
|
+
|
|
45
|
+
test('streaming row memo preserves plain append and resize measurements', () => {
|
|
46
|
+
const key = 'plain-row-memo';
|
|
47
|
+
const before = 'alpha\nbeta '.concat('x'.repeat(80));
|
|
48
|
+
const after = `${before}\ngamma ${'y'.repeat(120)}`;
|
|
49
|
+
|
|
50
|
+
assertCachedEqualsDirect(before, 40, key);
|
|
51
|
+
assertCachedEqualsDirect(after, 40, key);
|
|
52
|
+
assertCachedEqualsDirect(after, 24, key);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('streaming row memo preserves markdown stable-prefix measurements', () => {
|
|
56
|
+
const key = 'markdown-row-memo';
|
|
57
|
+
const before = '# Heading\n\nSettled paragraph.\n\n**live';
|
|
58
|
+
const after = `${before} suffix**`;
|
|
59
|
+
|
|
60
|
+
assertCachedEqualsDirect(before, 48, key);
|
|
61
|
+
assertCachedEqualsDirect(after, 48, key);
|
|
62
|
+
assert.equal(
|
|
63
|
+
measureStreamingMarkdownRenderedRows(after, 48, key),
|
|
64
|
+
measureStreamingMarkdownRenderedRowsUncached(after, 48, key),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('streaming row memo preserves fences, lists, and tables', () => {
|
|
69
|
+
const cases = [
|
|
70
|
+
['fence', ['# Intro\n\n```js\nconst value = 1;', '# Intro\n\n```js\nconst value = 1;\nconsole.log(value);']],
|
|
71
|
+
['list', ['- first item\n- second', '- first item\n- second item with wrapped detail '.repeat(3)]],
|
|
72
|
+
['table', ['| A | B |\n| - | - |\n| one | two |', '| A | B |\n| - | - |\n| one | two |\n| three | four |']],
|
|
73
|
+
];
|
|
74
|
+
for (const [name, values] of cases) {
|
|
75
|
+
for (const value of values) assertCachedEqualsDirect(value, 36, `shape-${name}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('render-mode changes reset the streaming estimate high-water', () => {
|
|
80
|
+
const id = 'render-mode-flip';
|
|
81
|
+
streamingEstimateRows({
|
|
82
|
+
id,
|
|
83
|
+
kind: 'assistant',
|
|
84
|
+
streaming: false,
|
|
85
|
+
text: 'large settled markdown paragraph '.repeat(80),
|
|
86
|
+
}, 24, false);
|
|
87
|
+
const flipped = streamingEstimateRows({
|
|
88
|
+
id,
|
|
89
|
+
kind: 'assistant',
|
|
90
|
+
streaming: true,
|
|
91
|
+
text: 'short',
|
|
92
|
+
}, 24, false);
|
|
93
|
+
const fresh = streamingEstimateRows({
|
|
94
|
+
id: 'render-mode-fresh',
|
|
95
|
+
kind: 'assistant',
|
|
96
|
+
streaming: true,
|
|
97
|
+
text: 'short',
|
|
98
|
+
}, 24, false);
|
|
99
|
+
assert.equal(flipped, fresh);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('stream estimate LRU evicts tail and high-water state in lockstep', () => {
|
|
103
|
+
const id = 'lru-evicted-stream';
|
|
104
|
+
streamingEstimateRows({
|
|
105
|
+
id,
|
|
106
|
+
kind: 'assistant',
|
|
107
|
+
streaming: true,
|
|
108
|
+
text: 'long streaming response '.repeat(80),
|
|
109
|
+
}, 24, false);
|
|
110
|
+
assert.deepEqual(streamingRowEstimateStateForId(id), {
|
|
111
|
+
tailEstimate: true,
|
|
112
|
+
highWater: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Production cleanup when no measured-height layout effect runs: admitting
|
|
116
|
+
// eight newer stream ids evicts this oldest id from the bounded estimate LRU.
|
|
117
|
+
for (let index = 0; index < 8; index++) {
|
|
118
|
+
streamingEstimateRows({
|
|
119
|
+
id: `lru-newer-${index}`,
|
|
120
|
+
kind: 'assistant',
|
|
121
|
+
streaming: true,
|
|
122
|
+
text: `newer ${index}`,
|
|
123
|
+
}, 24, false);
|
|
124
|
+
}
|
|
125
|
+
assert.deepEqual(streamingRowEstimateStateForId(id), {
|
|
126
|
+
tailEstimate: false,
|
|
127
|
+
highWater: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Reusing the evicted id must start from its new response, not retain the
|
|
131
|
+
// much taller high-water geometry from the aborted stream above.
|
|
132
|
+
const reused = streamingEstimateRows({
|
|
133
|
+
id,
|
|
134
|
+
kind: 'assistant',
|
|
135
|
+
streaming: true,
|
|
136
|
+
text: 'short',
|
|
137
|
+
}, 24, false);
|
|
138
|
+
const fresh = streamingEstimateRows({
|
|
139
|
+
id: 'lru-evicted-stream-fresh',
|
|
140
|
+
kind: 'assistant',
|
|
141
|
+
streaming: true,
|
|
142
|
+
text: 'short',
|
|
143
|
+
}, 24, false);
|
|
144
|
+
assert.equal(reused, fresh);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('cached tail rows preserve rendered-window bytes', () => {
|
|
148
|
+
const items = Array.from({ length: 20 }, (_, index) => ({
|
|
149
|
+
id: `notice-${index}`,
|
|
150
|
+
kind: 'notice',
|
|
151
|
+
text: `notice ${index}`,
|
|
152
|
+
}));
|
|
153
|
+
const tail = {
|
|
154
|
+
id: 'window-byte-tail',
|
|
155
|
+
kind: 'assistant',
|
|
156
|
+
streaming: true,
|
|
157
|
+
text: '# Result\n\n- first\n- second\n\n| A | B |\n| - | - |\n| one | two |',
|
|
158
|
+
};
|
|
159
|
+
const allItems = [...items, tail];
|
|
160
|
+
const cachedIndex = buildTranscriptRowIndex(allItems, { columns: 36 });
|
|
161
|
+
const directTailRows = 1 + measureStreamingMarkdownRenderedRowsUncached(tail.text, 36, tail.id);
|
|
162
|
+
const directRows = [...cachedIndex.rows.slice(0, -1), directTailRows];
|
|
163
|
+
const directPrefix = [0];
|
|
164
|
+
for (const rows of directRows) directPrefix.push(directPrefix.at(-1) + rows);
|
|
165
|
+
const directIndex = {
|
|
166
|
+
rows: directRows,
|
|
167
|
+
prefixRows: directPrefix,
|
|
168
|
+
totalRows: directPrefix.at(-1),
|
|
169
|
+
};
|
|
170
|
+
const options = { scrollOffset: 4, viewportHeight: 8, columns: 36 };
|
|
171
|
+
assert.equal(
|
|
172
|
+
JSON.stringify(transcriptRenderWindow(allItems, { ...options, rowIndex: cachedIndex })),
|
|
173
|
+
JSON.stringify(transcriptRenderWindow(allItems, { ...options, rowIndex: directIndex })),
|
|
174
|
+
);
|
|
175
|
+
});
|