@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.
- package/dist/assets/ToolOutputDialog-iiUOHO3c.js +16 -0
- package/dist/assets/index-BZ8pfXBh.css +1 -0
- package/dist/assets/index-DEj7Q-1y.js +2 -0
- package/dist/assets/{main-BFP0Fw2a.js → main-Ba2uuSTQ.js} +119 -119
- package/dist/assets/{vendor-.bun-CjZZibdK.js → vendor-.bun-B34wtB0D.js} +39 -39
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/lib/fs/routes.js +48 -0
- package/server/lib/opencode/proxy.js +106 -2
- package/server/lib/quota/DOCUMENTATION.md +1 -0
- package/server/lib/quota/index.js +2 -1
- package/server/lib/quota/providers/copilot.js +1 -1
- package/server/lib/quota/providers/index.js +8 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +141 -15
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -15
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/terminal/DOCUMENTATION.md +41 -80
- package/server/lib/terminal/index.js +27 -8
- package/server/lib/terminal/output-replay-buffer.js +66 -0
- package/server/lib/terminal/output-replay-buffer.test.js +66 -0
- package/server/lib/terminal/runtime.js +107 -20
- package/server/lib/terminal/{input-ws-protocol.js → terminal-ws-protocol.js} +13 -11
- package/server/lib/terminal/{input-ws-protocol.test.js → terminal-ws-protocol.test.js} +39 -32
- package/server/opencode-proxy.test.js +83 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/dist/assets/ToolOutputDialog-DwlX_M_n.js +0 -16
- package/dist/assets/index-BQqVuvn2.js +0 -2
- package/dist/assets/index-CH1IFYgs.css +0 -1
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +0 -44
- 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
|
|
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
|
|
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
|
-
- `
|
|
11
|
-
- `
|
|
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
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
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
|
-
- `
|
|
27
|
-
- `
|
|
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
|
-
- `
|
|
31
|
-
- `
|
|
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
|
-
###
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
54
|
-
- `
|
|
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
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
103
|
-
3.
|
|
104
|
-
4.
|
|
105
|
-
5. Verify
|
|
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
|
|
109
|
-
-
|
|
110
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
TERMINAL_WS_PATH,
|
|
3
|
+
TERMINAL_WS_CONTROL_TAG_JSON,
|
|
4
|
+
TERMINAL_WS_MAX_PAYLOAD_BYTES,
|
|
5
|
+
isTerminalWsPathname,
|
|
5
6
|
parseRequestPathname,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
normalizeTerminalWsMessageToBuffer,
|
|
8
|
+
normalizeTerminalWsMessageToText,
|
|
9
|
+
readTerminalWsControlFrame,
|
|
10
|
+
createTerminalWsControlFrame,
|
|
10
11
|
pruneRebindTimestamps,
|
|
11
12
|
isRebindRateLimited,
|
|
12
|
-
} from './
|
|
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
|
+
});
|