feat(terminal): refactor runtime and add mobile workspace (#2280)
Replace the legacy terminal flow with a shared authenticated WebSocket runtime used across web, desktop, relay, and mobile surfaces. - introduce the v3 terminal protocol with scoped attachments, snapshots, ordered output, bounded replay history, reconnects, and explicit lifecycle - harden PTY creation, restart, resize, close, force-kill, idle cleanup, shell selection, login mode, environment sanitization, and appearance sync - add runtime-aware terminal APIs with relay authentication and Electron parity - add a fullscreen mobile terminal workspace with touch scrolling, long-press selection, safe-area controls, quick keys, and Ctrl/Alt input - add terminal selection attachments, preview detection, project actions, shell settings, and localized UI - harden Ghostty rendering, resize recovery, Unicode handling, block characters, line height, and stale-row behavior - remove the obsolete terminal SSE path and update reverse-proxy guidance - expand terminal runtime, transport, input, selection, and store coverage - avoid duplicate web builds when preparing mobile assets in root CI builds
This commit is contained in:
committed by
GitHub
parent
f5b4a267c0
commit
d4a8c4d2e1
@@ -0,0 +1,58 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { useTerminalStore } from './useTerminalStore';
|
||||
|
||||
const setup = () => {
|
||||
useTerminalStore.getState().clearAll();
|
||||
useTerminalStore.getState().ensureDirectory('/repo');
|
||||
return useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0].id;
|
||||
};
|
||||
|
||||
describe('terminal state reconciliation', () => {
|
||||
afterEach(() => useTerminalStore.getState().clearAll());
|
||||
|
||||
test('applies snapshots atomically and deduplicates output by sequence', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'prompt', 4);
|
||||
useTerminalStore.getState().appendToBuffer('/repo', tabId, ' output', 5);
|
||||
useTerminalStore.getState().appendToBuffer('/repo', tabId, ' duplicate', 5);
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0];
|
||||
expect(tab.bufferChunks.map((chunk) => chunk.data).join('')).toBe('prompt output');
|
||||
expect(tab.lastSequence).toBe(5);
|
||||
});
|
||||
|
||||
test('keeps raw live bytes separate from replay-safe bytes', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().appendToBuffer('/repo', tabId, 'prompt\u001b[6n', 1, 'prompt');
|
||||
const chunk = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0].bufferChunks[0];
|
||||
expect(chunk.data).toBe('prompt\u001b[6n');
|
||||
expect(chunk.replayData).toBe('prompt');
|
||||
});
|
||||
|
||||
test('uses collision-resistant tab identities', () => {
|
||||
const tabId = setup();
|
||||
expect(/^tab-\d+$/.test(tabId)).toBe(false);
|
||||
});
|
||||
|
||||
test('does not let stale snapshots replace newer output', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'new', 8);
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'stale', 7);
|
||||
expect(useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0].bufferChunks[0].data).toBe('new');
|
||||
});
|
||||
|
||||
test('preserves buffer identity for an identical snapshot', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'prompt', 8);
|
||||
const previous = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0].bufferChunks;
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'prompt', 8);
|
||||
expect(useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0].bufferChunks).toBe(previous);
|
||||
});
|
||||
|
||||
test('caps multibyte scrollback by UTF-8 bytes', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().appendToBuffer('/repo', tabId, '界'.repeat(200_000), 1);
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0];
|
||||
expect(tab.bufferLength <= 512 * 1024).toBe(true);
|
||||
expect(new TextEncoder().encode(tab.bufferChunks[0].data).byteLength).toBe(tab.bufferLength);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user