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:
Bohdan Triapitsyn
2026-07-17 13:17:21 +03:00
committed by GitHub
parent f5b4a267c0
commit d4a8c4d2e1
103 changed files with 4085 additions and 4496 deletions
@@ -0,0 +1,42 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { useInlineCommentDraftStore } from './useInlineCommentDraftStore';
const selection = {
sessionKey: 'session-1',
source: 'terminal' as const,
fileLabel: 'Terminal 1',
startLine: 4,
endLine: 5,
code: 'first\nsecond',
language: 'term-1',
text: '',
};
describe('terminal context drafts', () => {
afterEach(() => { useInlineCommentDraftStore.setState({ drafts: {} }); });
test('persists snapshots by chat session and deduplicates identical selections', () => {
useInlineCommentDraftStore.getState().addDraft(selection);
useInlineCommentDraftStore.getState().addDraft(selection);
const drafts = useInlineCommentDraftStore.getState().getDrafts('session-1');
expect(drafts).toHaveLength(1);
expect({ ...drafts[0], id: undefined, createdAt: undefined }).toEqual({ ...selection, id: undefined, createdAt: undefined });
});
test('supports individual removal and ordered consume', () => {
useInlineCommentDraftStore.getState().addDraft(selection);
useInlineCommentDraftStore.getState().addDraft({ ...selection, startLine: 8, endLine: 8, code: 'third' });
const drafts = useInlineCommentDraftStore.getState().getDrafts('session-1');
useInlineCommentDraftStore.getState().removeDraft('session-1', drafts[0].id);
expect(useInlineCommentDraftStore.getState().consumeDrafts('session-1')).toHaveLength(1);
expect(useInlineCommentDraftStore.getState().getDrafts('session-1')).toEqual([]);
});
test('restores consumed drafts after a failed send without duplicating them', () => {
useInlineCommentDraftStore.getState().addDraft(selection);
const consumed = useInlineCommentDraftStore.getState().consumeDrafts('session-1');
useInlineCommentDraftStore.getState().restoreDrafts('session-1', consumed);
useInlineCommentDraftStore.getState().restoreDrafts('session-1', consumed);
expect(useInlineCommentDraftStore.getState().getDrafts('session-1')).toEqual(consumed);
});
});