@openchamber/web 1.9.2 → 1.9.3

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 (32) hide show
  1. package/dist/assets/ToolOutputDialog-iiUOHO3c.js +16 -0
  2. package/dist/assets/index-BZ8pfXBh.css +1 -0
  3. package/dist/assets/index-DEj7Q-1y.js +2 -0
  4. package/dist/assets/{main-BFP0Fw2a.js → main-Ba2uuSTQ.js} +119 -119
  5. package/dist/assets/{vendor-.bun-CjZZibdK.js → vendor-.bun-B34wtB0D.js} +39 -39
  6. package/dist/index.html +3 -3
  7. package/package.json +1 -1
  8. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  9. package/server/lib/fs/routes.js +48 -0
  10. package/server/lib/opencode/proxy.js +106 -2
  11. package/server/lib/quota/DOCUMENTATION.md +1 -0
  12. package/server/lib/quota/index.js +2 -1
  13. package/server/lib/quota/providers/copilot.js +1 -1
  14. package/server/lib/quota/providers/index.js +8 -0
  15. package/server/lib/quota/providers/minimax-cn-coding-plan.js +141 -15
  16. package/server/lib/quota/providers/minimax-coding-plan.js +139 -15
  17. package/server/lib/quota/providers/zhipuai.js +114 -0
  18. package/server/lib/terminal/DOCUMENTATION.md +41 -80
  19. package/server/lib/terminal/index.js +27 -8
  20. package/server/lib/terminal/output-replay-buffer.js +66 -0
  21. package/server/lib/terminal/output-replay-buffer.test.js +66 -0
  22. package/server/lib/terminal/runtime.js +107 -20
  23. package/server/lib/terminal/{input-ws-protocol.js → terminal-ws-protocol.js} +13 -11
  24. package/server/lib/terminal/{input-ws-protocol.test.js → terminal-ws-protocol.test.js} +39 -32
  25. package/server/opencode-proxy.test.js +83 -0
  26. package/server/proxy-headers.js +61 -0
  27. package/server/proxy-headers.test.js +58 -0
  28. package/dist/assets/ToolOutputDialog-DwlX_M_n.js +0 -16
  29. package/dist/assets/index-BQqVuvn2.js +0 -2
  30. package/dist/assets/index-CH1IFYgs.css +0 -1
  31. package/server/TERMINAL_INPUT_WS_PROTOCOL.md +0 -44
  32. package/server/lib/quota/providers/minimax-shared.js +0 -136
@@ -0,0 +1,114 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import { readConfigLayers } from '../../opencode/shared.js';
3
+ import {
4
+ getAuthEntry,
5
+ normalizeAuthEntry,
6
+ buildResult,
7
+ toUsageWindow,
8
+ toNumber,
9
+ toTimestamp,
10
+ resolveWindowSeconds,
11
+ resolveWindowLabel,
12
+ normalizeTimestamp
13
+ } from '../utils/index.js';
14
+
15
+ export const providerId = 'zhipuai-coding-plan';
16
+ export const providerName = 'ZhipuAI';
17
+ export const aliases = ['zhipuai-coding-plan', 'zhipuai', 'zhipu'];
18
+
19
+ function getApiKey() {
20
+ const auth = readAuthFile();
21
+ const oldEntry = normalizeAuthEntry(getAuthEntry(auth, aliases));
22
+ const apiKeyFromOld = oldEntry?.key ?? oldEntry?.token;
23
+
24
+ if (apiKeyFromOld) {
25
+ return apiKeyFromOld;
26
+ }
27
+
28
+ try {
29
+ const layers = readConfigLayers();
30
+ const { mergedConfig } = layers;
31
+
32
+ for (const alias of aliases) {
33
+ const providerConfig = mergedConfig?.provider?.[alias];
34
+ if (providerConfig?.options?.apiKey) {
35
+ return providerConfig.options.apiKey;
36
+ }
37
+ }
38
+ } catch (error) {
39
+ // Ignore read errors
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ export const isConfigured = () => {
46
+ return Boolean(getApiKey());
47
+ };
48
+
49
+ export const fetchQuota = async () => {
50
+ const apiKey = getApiKey();
51
+
52
+ if (!apiKey) {
53
+ return buildResult({
54
+ providerId,
55
+ providerName,
56
+ ok: false,
57
+ configured: false,
58
+ error: 'Not configured'
59
+ });
60
+ }
61
+
62
+ try {
63
+ const response = await fetch('https://open.bigmodel.cn/api/monitor/usage/quota/limit', {
64
+ method: 'GET',
65
+ headers: {
66
+ Authorization: `Bearer ${apiKey}`,
67
+ 'Content-Type': 'application/json'
68
+ }
69
+ });
70
+
71
+ if (!response.ok) {
72
+ return buildResult({
73
+ providerId,
74
+ providerName,
75
+ ok: false,
76
+ configured: true,
77
+ error: `API error: ${response.status}`
78
+ });
79
+ }
80
+
81
+ const payload = await response.json();
82
+ const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
83
+ const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT');
84
+ const windowSeconds = resolveWindowSeconds(tokensLimit);
85
+ const windowLabel = resolveWindowLabel(windowSeconds);
86
+ const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
87
+ const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null;
88
+
89
+ const windows = {};
90
+ if (tokensLimit) {
91
+ windows[windowLabel] = toUsageWindow({
92
+ usedPercent,
93
+ windowSeconds,
94
+ resetAt
95
+ });
96
+ }
97
+
98
+ return buildResult({
99
+ providerId,
100
+ providerName,
101
+ ok: true,
102
+ configured: true,
103
+ usage: { windows }
104
+ });
105
+ } catch (error) {
106
+ return buildResult({
107
+ providerId,
108
+ providerName,
109
+ ok: false,
110
+ configured: true,
111
+ error: error instanceof Error ? error.message : 'Request failed'
112
+ });
113
+ }
114
+ };
@@ -1,115 +1,76 @@
1
1
  # Terminal Module Documentation
2
2
 
3
3
  ## Purpose
4
- This module provides WebSocket protocol utilities for terminal input handling in the web server runtime, including message normalization, control frame parsing, rate limiting, and pathname resolution for terminal WebSocket connections.
4
+ This module provides WebSocket transport utilities for terminal input and output in the web server runtime, including message normalization, control frame parsing, rate limiting, pathname resolution, and short-lived output replay buffering for terminal WebSocket connections.
5
5
 
6
6
  ## Entrypoints and structure
7
7
  - `packages/web/server/lib/terminal/`: Terminal module directory.
8
- - `index.js`: Stable module entrypoint that re-exports protocol helpers/constants.
8
+ - `index.js`: Stable module entrypoint that re-exports protocol helpers and replay-buffer helpers.
9
9
  - `runtime.js`: Runtime module that owns terminal session state, WS server setup, and `/api/terminal/*` route registration.
10
- - `input-ws-protocol.js`: Single-file module containing all terminal input WebSocket protocol utilities.
11
- - `packages/web/server/lib/terminal/input-ws-protocol.test.js`: Test file for protocol utilities.
10
+ - `terminal-ws-protocol.js`: Single-file module containing terminal WebSocket protocol utilities.
11
+ - `output-replay-buffer.js`: Helper module for buffering recent terminal output so late subscribers can receive startup prompt data.
12
+ - `packages/web/server/lib/terminal/terminal-ws-protocol.test.js`: Test file for protocol utilities.
13
+ - `packages/web/server/lib/terminal/output-replay-buffer.test.js`: Test file for replay buffer helpers.
12
14
 
13
15
  Public API entry point: imported by `packages/web/server/index.js` from `./lib/terminal/index.js`.
14
16
 
15
17
  ## Public exports
16
18
 
17
19
  ### Constants
18
- - `TERMINAL_INPUT_WS_PATH`: WebSocket endpoint path (`/api/terminal/input-ws`).
19
- - `TERMINAL_INPUT_WS_CONTROL_TAG_JSON`: Control frame tag byte (0x01) indicating JSON payload.
20
- - `TERMINAL_INPUT_WS_MAX_PAYLOAD_BYTES`: Maximum payload size (64KB).
20
+ - `TERMINAL_WS_PATH`: Primary WebSocket endpoint path (`/api/terminal/ws`).
21
+ - `TERMINAL_WS_CONTROL_TAG_JSON`: Control frame tag byte (`0x01`) indicating JSON payload.
22
+ - `TERMINAL_WS_MAX_PAYLOAD_BYTES`: Maximum inbound WebSocket payload size (64KB).
23
+ - `TERMINAL_OUTPUT_REPLAY_MAX_BYTES`: Maximum buffered terminal output retained for replay (64KB).
21
24
 
22
25
  ### Request Parsing
23
26
  - `parseRequestPathname(requestUrl)`: Extracts pathname from request URL string. Returns empty string for invalid inputs.
27
+ - `isTerminalWsPathname(pathname)`: Returns whether a pathname matches a supported terminal WebSocket route.
24
28
 
25
29
  ### Message Normalization
26
- - `normalizeTerminalInputWsMessageToBuffer(rawData)`: Normalizes various data types (Buffer, Uint8Array, ArrayBuffer, string, chunk arrays) to a single Buffer.
27
- - `normalizeTerminalInputWsMessageToText(rawData)`: Normalizes data to UTF-8 text string. Passes through strings directly, converts binary data to text.
30
+ - `normalizeTerminalWsMessageToBuffer(rawData)`: Normalizes various data types (Buffer, Uint8Array, ArrayBuffer, string, chunk arrays) to a single Buffer.
31
+ - `normalizeTerminalWsMessageToText(rawData)`: Normalizes data to UTF-8 text string.
28
32
 
29
33
  ### Control Frame Handling
30
- - `readTerminalInputWsControlFrame(rawData)`: Parses WebSocket message as control frame. Returns parsed JSON object or null if invalid/malformed. Validates control tag prefix and JSON structure.
31
- - `createTerminalInputWsControlFrame(payload)`: Creates a control frame with JSON payload. Prepends control tag byte.
34
+ - `readTerminalWsControlFrame(rawData)`: Parses WebSocket message as control frame. Returns parsed JSON object or null if invalid or malformed.
35
+ - `createTerminalWsControlFrame(payload)`: Creates a control frame with JSON payload and prepends the control tag byte.
32
36
 
33
- ### Rate Limiting
34
- - `pruneRebindTimestamps(timestamps, now, windowMs)`: Filters timestamps to keep only those within the active time window.
35
- - `isRebindRateLimited(timestamps, maxPerWindow)`: Checks if rebind operations have exceeded rate limit threshold.
36
-
37
- ## Response contracts
38
-
39
- ### Control Frame
40
- Control frames use binary encoding:
41
- - First byte: `TERMINAL_INPUT_WS_CONTROL_TAG_JSON` (0x01)
42
- - Remaining bytes: UTF-8 encoded JSON object
43
- - Parsed result: Object or null on parse failure
44
-
45
- ### Normalized Buffer
46
- Input types are normalized to Buffer:
47
- - `Buffer`: Returned as-is
48
- - `Uint8Array`/`ArrayBuffer`: Converted to Buffer
49
- - `String`: Converted to UTF-8 Buffer
50
- - `Array<Buffer|string|Uint8Array>`: Concatenated to single Buffer
37
+ ### Replay Buffer Helpers
38
+ - `createTerminalOutputReplayBuffer()`: Creates mutable state for recent terminal output replay.
39
+ - `appendTerminalOutputReplayChunk(bufferState, data, maxBytes?)`: Appends a chunk, trimming older buffered data to stay within the configured byte budget.
40
+ - `listTerminalOutputReplayChunksSince(bufferState, lastSeenId)`: Returns buffered chunks newer than the provided replay cursor.
41
+ - `getLatestTerminalOutputReplayChunkId(bufferState)`: Returns the latest chunk id in the replay buffer, or `0` when empty.
51
42
 
52
43
  ### Rate Limiting
53
- Rate limiting uses timestamp arrays:
54
- - `pruneRebindTimestamps`: Returns filtered array of active timestamps
55
- - `isRebindRateLimited`: Returns boolean indicating if limit is reached
44
+ - `pruneRebindTimestamps(timestamps, now, windowMs)`: Filters timestamps to keep only those within the active time window.
45
+ - `isRebindRateLimited(timestamps, maxPerWindow)`: Checks if rebind operations have exceeded the configured threshold.
56
46
 
57
47
  ## Usage in web server
58
-
59
- The terminal protocol utilities are used by `packages/web/server/index.js` for:
60
- - WebSocket endpoint path definition (`TERMINAL_INPUT_WS_PATH`)
61
- - Message normalization for input handling
62
- - Control frame parsing for session binding
48
+ The terminal helpers are used by `packages/web/server/index.js` for:
49
+ - WebSocket endpoint path definition and matching
50
+ - Message normalization for terminal input payloads
51
+ - Control frame parsing for session binding, keepalive, and exit signaling
63
52
  - Rate limiting for session rebind operations
64
53
  - Request pathname parsing for WebSocket routing
54
+ - Replaying startup output such as shell prompts when the client binds after the PTY already emitted data
65
55
 
66
- The web server uses these utilities in combination with `bun-pty` or `node-pty` for PTY session management.
56
+ The web server combines these utilities with `bun-pty` or `node-pty` to drive full-duplex PTY sessions.
67
57
 
68
58
  ## Notes for contributors
69
-
70
- ### Adding New Control Frame Types
71
- 1. Define new control tag constants (e.g., `TERMINAL_INPUT_WS_CONTROL_TAG_CUSTOM = 0x02`)
72
- 2. Update `readTerminalInputWsControlFrame` to handle new tag type
73
- 3. Update `createTerminalInputWsControlFrame` or create new frame creation function
74
- 4. Add corresponding tests in `terminal-input-ws-protocol.test.js`
75
-
76
- ### Message Normalization
77
- - Always normalize incoming WebSocket messages before processing
78
- - Use `normalizeTerminalInputWsMessageToBuffer` for binary data
79
- - Use `normalizeTerminalInputWsMessageToText` for text data (terminal escape sequences)
80
- - Normalize chunked messages from WebSocket fragmentation handling
81
-
82
- ### Rate Limiting
83
- - Rate limiting is time-window based: tracks timestamps within a rolling window
84
- - Use `pruneRebindTimestamps` to clean up stale timestamps before rate limit checks
85
- - Configure `maxPerWindow` based on operational requirements (prevent abuse)
86
-
87
- ### Error Handling
88
- - `readTerminalInputWsControlFrame` returns null for invalid/malformed frames
89
- - `parseRequestPathname` returns empty string for invalid URLs
90
- - Callers should handle null/empty returns gracefully
91
-
92
- ### Testing
93
- - Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing changes
94
- - Test edge cases: empty payloads, malformed JSON, chunked messages, rate limit boundaries
95
- - Verify control frame roundtrip: create → read → validate payload equality
96
- - Test pathname parsing with relative URLs, absolute URLs, and invalid inputs
59
+ - Keep control frames backward-compatible when possible; use explicit `v` values for protocol changes.
60
+ - Always normalize incoming WebSocket messages before processing them.
61
+ - Keep replay buffering small and memory-only; it exists to cover startup races, not to implement persistent scrollback.
62
+ - Add tests for new control frame types, websocket path changes, malformed payload handling, and replay trimming semantics.
63
+ - Keep HTTP input and SSE output fallbacks functional unless the rollout explicitly removes them.
97
64
 
98
65
  ## Verification notes
99
-
100
66
  ### Manual verification
101
- 1. Start web server and create terminal session via `/api/terminal/create`
102
- 2. Connect to `/api/terminal/input-ws` WebSocket
103
- 3. Send control frames with valid/invalid payloads to verify parsing
104
- 4. Test message normalization with various data types
105
- 5. Verify rate limiting by issuing rapid rebind requests
67
+ 1. Start the web server and create a terminal session via `/api/terminal/create`.
68
+ 2. Wait briefly before binding the client to ensure the shell emits its prompt first.
69
+ 3. Connect to `/api/terminal/ws` WebSocket and bind to the session.
70
+ 4. Verify the startup prompt and early shell output are replayed before interactive input begins.
71
+ 5. Verify `/api/terminal/input-ws` is rejected with `404 Not Found` and `/api/terminal/:sessionId/stream` still works as a fallback path.
106
72
 
107
73
  ### Automated verification
108
- - Run test file: `bun test packages/web/server/lib/terminal/input-ws-protocol.test.js`
109
- - Protocol tests should pass covering:
110
- - WebSocket path constant
111
- - Control frame encoding/decoding
112
- - Payload validation
113
- - Message normalization (all data types)
114
- - Pathname parsing
115
- - Rate limiting logic
74
+ - Run `bun test packages/web/server/lib/terminal/terminal-ws-protocol.test.js`
75
+ - Run `bun test packages/web/server/lib/terminal/output-replay-buffer.test.js`
76
+ - Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing changes.
@@ -1,12 +1,31 @@
1
1
  export {
2
- TERMINAL_INPUT_WS_PATH,
3
- TERMINAL_INPUT_WS_CONTROL_TAG_JSON,
4
- TERMINAL_INPUT_WS_MAX_PAYLOAD_BYTES,
2
+ TERMINAL_WS_PATH,
3
+ TERMINAL_WS_CONTROL_TAG_JSON,
4
+ TERMINAL_WS_MAX_PAYLOAD_BYTES,
5
+ isTerminalWsPathname,
5
6
  parseRequestPathname,
6
- normalizeTerminalInputWsMessageToBuffer,
7
- normalizeTerminalInputWsMessageToText,
8
- readTerminalInputWsControlFrame,
9
- createTerminalInputWsControlFrame,
7
+ normalizeTerminalWsMessageToBuffer,
8
+ normalizeTerminalWsMessageToText,
9
+ readTerminalWsControlFrame,
10
+ createTerminalWsControlFrame,
10
11
  pruneRebindTimestamps,
11
12
  isRebindRateLimited,
12
- } from './input-ws-protocol.js';
13
+ } from './terminal-ws-protocol.js';
14
+
15
+ export {
16
+ TERMINAL_WS_PATH as TERMINAL_INPUT_WS_PATH,
17
+ TERMINAL_WS_CONTROL_TAG_JSON as TERMINAL_INPUT_WS_CONTROL_TAG_JSON,
18
+ TERMINAL_WS_MAX_PAYLOAD_BYTES as TERMINAL_INPUT_WS_MAX_PAYLOAD_BYTES,
19
+ normalizeTerminalWsMessageToBuffer as normalizeTerminalInputWsMessageToBuffer,
20
+ normalizeTerminalWsMessageToText as normalizeTerminalInputWsMessageToText,
21
+ readTerminalWsControlFrame as readTerminalInputWsControlFrame,
22
+ createTerminalWsControlFrame as createTerminalInputWsControlFrame,
23
+ } from './terminal-ws-protocol.js';
24
+
25
+ export {
26
+ TERMINAL_OUTPUT_REPLAY_MAX_BYTES,
27
+ createTerminalOutputReplayBuffer,
28
+ appendTerminalOutputReplayChunk,
29
+ listTerminalOutputReplayChunksSince,
30
+ getLatestTerminalOutputReplayChunkId,
31
+ } from './output-replay-buffer.js';
@@ -0,0 +1,66 @@
1
+ export const TERMINAL_OUTPUT_REPLAY_MAX_BYTES = 64 * 1024;
2
+
3
+ const trimTerminalOutputChunkToMaxBytes = (data, maxBytes) => {
4
+ if (typeof data !== 'string' || data.length === 0) {
5
+ return '';
6
+ }
7
+
8
+ const bytes = Buffer.byteLength(data, 'utf8');
9
+ if (bytes <= maxBytes) {
10
+ return data;
11
+ }
12
+
13
+ const trimmedBuffer = Buffer.from(data, 'utf8').subarray(-maxBytes);
14
+ return trimmedBuffer.toString('utf8');
15
+ };
16
+
17
+ export const createTerminalOutputReplayBuffer = () => ({
18
+ chunks: [],
19
+ totalBytes: 0,
20
+ nextId: 1,
21
+ });
22
+
23
+ export const appendTerminalOutputReplayChunk = (bufferState, data, maxBytes = TERMINAL_OUTPUT_REPLAY_MAX_BYTES) => {
24
+ if (!bufferState || typeof bufferState !== 'object') {
25
+ return null;
26
+ }
27
+
28
+ const normalizedData = trimTerminalOutputChunkToMaxBytes(data, maxBytes);
29
+ if (!normalizedData) {
30
+ return null;
31
+ }
32
+
33
+ const bytes = Buffer.byteLength(normalizedData, 'utf8');
34
+ const chunk = {
35
+ id: bufferState.nextId,
36
+ data: normalizedData,
37
+ bytes,
38
+ };
39
+
40
+ bufferState.nextId += 1;
41
+ bufferState.chunks.push(chunk);
42
+ bufferState.totalBytes += bytes;
43
+
44
+ while (bufferState.totalBytes > maxBytes && bufferState.chunks.length > 1) {
45
+ const removedChunk = bufferState.chunks.shift();
46
+ bufferState.totalBytes -= removedChunk?.bytes ?? 0;
47
+ }
48
+
49
+ return chunk;
50
+ };
51
+
52
+ export const listTerminalOutputReplayChunksSince = (bufferState, lastSeenId = 0) => {
53
+ if (!bufferState || typeof bufferState !== 'object' || !Array.isArray(bufferState.chunks)) {
54
+ return [];
55
+ }
56
+
57
+ return bufferState.chunks.filter((chunk) => chunk.id > lastSeenId);
58
+ };
59
+
60
+ export const getLatestTerminalOutputReplayChunkId = (bufferState) => {
61
+ if (!bufferState || typeof bufferState !== 'object' || !Array.isArray(bufferState.chunks) || bufferState.chunks.length === 0) {
62
+ return 0;
63
+ }
64
+
65
+ return bufferState.chunks[bufferState.chunks.length - 1]?.id ?? 0;
66
+ };
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import {
4
+ TERMINAL_OUTPUT_REPLAY_MAX_BYTES,
5
+ appendTerminalOutputReplayChunk,
6
+ createTerminalOutputReplayBuffer,
7
+ getLatestTerminalOutputReplayChunkId,
8
+ listTerminalOutputReplayChunksSince,
9
+ } from './output-replay-buffer.js';
10
+
11
+ describe('terminal output replay buffer', () => {
12
+ it('starts empty', () => {
13
+ const bufferState = createTerminalOutputReplayBuffer();
14
+ expect(bufferState).toEqual({ chunks: [], totalBytes: 0, nextId: 1 });
15
+ expect(getLatestTerminalOutputReplayChunkId(bufferState)).toBe(0);
16
+ });
17
+
18
+ it('appends chunks with incrementing ids', () => {
19
+ const bufferState = createTerminalOutputReplayBuffer();
20
+ const first = appendTerminalOutputReplayChunk(bufferState, 'prompt> ');
21
+ const second = appendTerminalOutputReplayChunk(bufferState, 'ls\r\n');
22
+
23
+ expect(first).toEqual({ id: 1, data: 'prompt> ', bytes: 8 });
24
+ expect(second).toEqual({ id: 2, data: 'ls\r\n', bytes: 4 });
25
+ expect(getLatestTerminalOutputReplayChunkId(bufferState)).toBe(2);
26
+ });
27
+
28
+ it('lists chunks after a replay cursor', () => {
29
+ const bufferState = createTerminalOutputReplayBuffer();
30
+ appendTerminalOutputReplayChunk(bufferState, 'prompt> ');
31
+ appendTerminalOutputReplayChunk(bufferState, 'ls\r\n');
32
+ appendTerminalOutputReplayChunk(bufferState, 'file.txt\r\n');
33
+
34
+ expect(listTerminalOutputReplayChunksSince(bufferState, 1).map((chunk) => chunk.data)).toEqual([
35
+ 'ls\r\n',
36
+ 'file.txt\r\n',
37
+ ]);
38
+ });
39
+
40
+ it('trims old chunks beyond max bytes', () => {
41
+ const bufferState = createTerminalOutputReplayBuffer();
42
+ appendTerminalOutputReplayChunk(bufferState, '1234', 8);
43
+ appendTerminalOutputReplayChunk(bufferState, '5678', 8);
44
+ appendTerminalOutputReplayChunk(bufferState, '90', 8);
45
+
46
+ expect(bufferState.chunks.map((chunk) => chunk.data)).toEqual(['5678', '90']);
47
+ expect(bufferState.totalBytes).toBe(6);
48
+ });
49
+
50
+ it('trims oversized single chunks to the configured max bytes', () => {
51
+ const bufferState = createTerminalOutputReplayBuffer();
52
+ const chunk = appendTerminalOutputReplayChunk(bufferState, 'abcdefghij', 4);
53
+
54
+ expect(chunk?.data).toBe('ghij');
55
+ expect(chunk?.bytes).toBe(4);
56
+ expect(bufferState.totalBytes).toBe(4);
57
+ });
58
+
59
+ it('uses the default max bytes when not provided', () => {
60
+ const bufferState = createTerminalOutputReplayBuffer();
61
+ const chunk = appendTerminalOutputReplayChunk(bufferState, 'ok');
62
+
63
+ expect(chunk?.bytes).toBe(2);
64
+ expect(TERMINAL_OUTPUT_REPLAY_MAX_BYTES).toBe(64 * 1024);
65
+ });
66
+ });