gitspace 0.2.0-rc.1
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/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# Connection & State Management
|
|
2
|
+
|
|
3
|
+
This document describes how GitSpace handles WebSocket connections, disconnections, and terminal state synchronization.
|
|
4
|
+
|
|
5
|
+
> **Related:** See [PROTOCOL.md](./PROTOCOL.md) for message/frame format, [REMOTE-DESIGN.md](./REMOTE-DESIGN.md) for security model.
|
|
6
|
+
|
|
7
|
+
> **Implementation Note:** The web client (`src/web/`) currently lacks the
|
|
8
|
+
> auto-reconnection logic described below. While the relay connection has a
|
|
9
|
+
> 15-second heartbeat, neither the relay nor terminal connections implement
|
|
10
|
+
> exponential backoff reconnection. This is a known gap - disconnections
|
|
11
|
+
> require manual user intervention to reconnect.
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
GitSpace uses a **stateful server, stateless client** model:
|
|
16
|
+
|
|
17
|
+
- **Server (tmux-lite)**: Maintains authoritative terminal state via xterm-headless
|
|
18
|
+
- **Client (browser/CLI)**: Just a view into server state, can be discarded and rebuilt
|
|
19
|
+
|
|
20
|
+
This means connection drops are trivial to handle - the client simply reconnects and gets the current state.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ │
|
|
25
|
+
│ Server (your machine) Client (anywhere) │
|
|
26
|
+
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
|
27
|
+
│ │ │ │ │ │
|
|
28
|
+
│ │ xterm-headless │ │ xterm.js │ │
|
|
29
|
+
│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │
|
|
30
|
+
│ │ │ $ npm run dev │ │◀─────▶│ │ $ npm run dev │ │ │
|
|
31
|
+
│ │ │ > ready on :3000 │ │ wss │ │ > ready on :3000 │ │ │
|
|
32
|
+
│ │ │ █ │ │ │ │ █ │ │ │
|
|
33
|
+
│ │ └───────────────────────┘ │ │ └───────────────────────┘ │ │
|
|
34
|
+
│ │ │ │ │ │
|
|
35
|
+
│ │ AUTHORITATIVE STATE │ │ DISPOSABLE VIEW │ │
|
|
36
|
+
│ │ (survives disconnects) │ │ (rebuilt on reconnect) │ │
|
|
37
|
+
│ │ │ │ │ │
|
|
38
|
+
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
|
39
|
+
│ │
|
|
40
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Why Terminals Are Different
|
|
44
|
+
|
|
45
|
+
Unlike chat or event streams, terminals don't need message replay:
|
|
46
|
+
|
|
47
|
+
| Chat Stream | Terminal |
|
|
48
|
+
|-------------|----------|
|
|
49
|
+
| Every message matters | Only current screen matters |
|
|
50
|
+
| Miss a message = data loss | Miss output = just refresh |
|
|
51
|
+
| Need exactly-once delivery | Need current state |
|
|
52
|
+
| Offset-based replay | Screen buffer sync |
|
|
53
|
+
|
|
54
|
+
**The terminal screen is the data.** Old output that scrolled off is gone (or in scrollback). When reconnecting, the client just needs to know what's on screen right now.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Connection Lifecycle
|
|
59
|
+
|
|
60
|
+
### Initial Connection
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Client Relay Server
|
|
64
|
+
│ │ │
|
|
65
|
+
│ 1. Connect to relay │ │
|
|
66
|
+
│ ─────────────────────────▶ │ │
|
|
67
|
+
│ │ │
|
|
68
|
+
│ 2. Sign connect message │ │
|
|
69
|
+
│ ─────────────────────────▶ │ │
|
|
70
|
+
│ │ 3. Route to machine │
|
|
71
|
+
│ │ ─────────────────────────▶ │
|
|
72
|
+
│ │ │
|
|
73
|
+
│ 4. X3DH handshake (4 phases) │ │
|
|
74
|
+
│ ◀════════════════════════════╪═══════════════════════════▶ │
|
|
75
|
+
│ │ │
|
|
76
|
+
│ 5. E2E encrypted channel established │
|
|
77
|
+
│ ◀════════════════════════════╪═══════════════════════════▶ │
|
|
78
|
+
│ │ │
|
|
79
|
+
│ 6. Attach to session (encrypted) │
|
|
80
|
+
│ ═══════════════════════════════════════════════════════▶ │
|
|
81
|
+
│ │ │
|
|
82
|
+
│ 7. State sync (encrypted) │ │
|
|
83
|
+
│ ◀═══════════════════════════════════════════════════════ │
|
|
84
|
+
│ │ │
|
|
85
|
+
│ 8. Stream output (encrypted) │ │
|
|
86
|
+
│ ◀═══════════════════════════════════════════════════════ │
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [PROTOCOL.md](./PROTOCOL.md) for X3DH handshake details.
|
|
90
|
+
|
|
91
|
+
### Disconnection & Reconnection
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Client Relay Server
|
|
95
|
+
│ │ │
|
|
96
|
+
│ streaming... │ │
|
|
97
|
+
│ ◀════════════════════════════╪═══════════════════════════ │
|
|
98
|
+
│ │ │
|
|
99
|
+
╳ NETWORK DIES │ │
|
|
100
|
+
│ │ │
|
|
101
|
+
│ │ (relay notices client gone) │
|
|
102
|
+
│ │ (server keeps running) │
|
|
103
|
+
│ │ (xterm-headless continues) │
|
|
104
|
+
│ │ │
|
|
105
|
+
│ (client detects disconnect) │ │
|
|
106
|
+
│ (exponential backoff...) │ │
|
|
107
|
+
│ │ │
|
|
108
|
+
│ Reconnect │ │
|
|
109
|
+
│ ─────────────────────────▶ │ │
|
|
110
|
+
│ │ │
|
|
111
|
+
│ Re-sign connect message │ │
|
|
112
|
+
│ ─────────────────────────▶ │ │
|
|
113
|
+
│ │ │
|
|
114
|
+
│ Re-attach (same session) │ │
|
|
115
|
+
│ ═══════════════════════════════════════════════════════▶ │
|
|
116
|
+
│ │ │
|
|
117
|
+
│ State sync (CURRENT screen) │ │
|
|
118
|
+
│ ◀═══════════════════════════════════════════════════════ │
|
|
119
|
+
│ │ │
|
|
120
|
+
│ Resume streaming │ │
|
|
121
|
+
│ ◀════════════════════════════╪═══════════════════════════ │
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Key insight:** The server doesn't care that the client was gone. It just sends current state when asked.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## State Sync Protocol
|
|
129
|
+
|
|
130
|
+
### Attach Request
|
|
131
|
+
|
|
132
|
+
When a client attaches to a session (initial or reconnect):
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
interface AttachRequest {
|
|
136
|
+
type: 'attach';
|
|
137
|
+
sessionId: string;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### State Sync Response
|
|
142
|
+
|
|
143
|
+
Server responds with complete terminal state:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
interface StateSync {
|
|
147
|
+
type: 'state-sync';
|
|
148
|
+
|
|
149
|
+
// Current screen buffer (ANSI-encoded)
|
|
150
|
+
screen: string;
|
|
151
|
+
|
|
152
|
+
// Cursor position
|
|
153
|
+
cursorX: number;
|
|
154
|
+
cursorY: number;
|
|
155
|
+
|
|
156
|
+
// Terminal dimensions
|
|
157
|
+
cols: number;
|
|
158
|
+
rows: number;
|
|
159
|
+
|
|
160
|
+
// Scrollback buffer (last N lines, ANSI-encoded)
|
|
161
|
+
scrollback: string;
|
|
162
|
+
|
|
163
|
+
// Process info
|
|
164
|
+
title: string; // Current process title
|
|
165
|
+
cwd: string; // Current working directory
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Incremental Output
|
|
170
|
+
|
|
171
|
+
After sync, server streams incremental output:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
interface Output {
|
|
175
|
+
type: 'output';
|
|
176
|
+
data: Uint8Array; // Raw terminal output (ANSI sequences, text, etc.)
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Client Implementation
|
|
183
|
+
|
|
184
|
+
### Connection Manager
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
class ConnectionManager {
|
|
188
|
+
private ws: WebSocket | null = null;
|
|
189
|
+
private sessionId: string;
|
|
190
|
+
private reconnectAttempts = 0;
|
|
191
|
+
private maxReconnectDelay = 30000; // 30 seconds max
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
private relayUrl: string,
|
|
195
|
+
private credentials: Credentials,
|
|
196
|
+
private terminal: Terminal,
|
|
197
|
+
) {}
|
|
198
|
+
|
|
199
|
+
async connect(sessionId: string) {
|
|
200
|
+
this.sessionId = sessionId;
|
|
201
|
+
await this.establishConnection();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async establishConnection() {
|
|
205
|
+
try {
|
|
206
|
+
this.ws = new WebSocket(this.relayUrl);
|
|
207
|
+
|
|
208
|
+
this.ws.onopen = () => this.handleOpen();
|
|
209
|
+
this.ws.onclose = () => this.handleClose();
|
|
210
|
+
this.ws.onerror = (e) => this.handleError(e);
|
|
211
|
+
this.ws.onmessage = (e) => this.handleMessage(e);
|
|
212
|
+
|
|
213
|
+
} catch (err) {
|
|
214
|
+
this.scheduleReconnect();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private handleOpen() {
|
|
219
|
+
this.reconnectAttempts = 0;
|
|
220
|
+
|
|
221
|
+
// Authenticate
|
|
222
|
+
this.send({
|
|
223
|
+
type: 'auth',
|
|
224
|
+
apiKey: this.credentials.apiKey,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Attach to session
|
|
228
|
+
this.send({
|
|
229
|
+
type: 'attach',
|
|
230
|
+
sessionId: this.sessionId,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private handleClose() {
|
|
235
|
+
this.ws = null;
|
|
236
|
+
this.terminal.showDisconnected();
|
|
237
|
+
this.scheduleReconnect();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private handleError(error: Event) {
|
|
241
|
+
console.error('WebSocket error:', error);
|
|
242
|
+
// onclose will fire next, triggering reconnect
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private handleMessage(event: MessageEvent) {
|
|
246
|
+
const msg = this.decrypt(event.data);
|
|
247
|
+
|
|
248
|
+
switch (msg.type) {
|
|
249
|
+
case 'state-sync':
|
|
250
|
+
this.handleStateSync(msg);
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'output':
|
|
254
|
+
this.terminal.write(msg.data);
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case 'error':
|
|
258
|
+
this.handleServerError(msg);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private handleStateSync(sync: StateSync) {
|
|
264
|
+
// Clear terminal and render current state
|
|
265
|
+
this.terminal.reset();
|
|
266
|
+
this.terminal.resize(sync.cols, sync.rows);
|
|
267
|
+
|
|
268
|
+
// Write scrollback first (if any)
|
|
269
|
+
if (sync.scrollback) {
|
|
270
|
+
this.terminal.write(sync.scrollback);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Write current screen
|
|
274
|
+
this.terminal.write(sync.screen);
|
|
275
|
+
|
|
276
|
+
// Position cursor
|
|
277
|
+
this.terminal.setCursor(sync.cursorX, sync.cursorY);
|
|
278
|
+
|
|
279
|
+
this.terminal.showConnected();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private scheduleReconnect() {
|
|
283
|
+
const delay = Math.min(
|
|
284
|
+
1000 * Math.pow(2, this.reconnectAttempts),
|
|
285
|
+
this.maxReconnectDelay
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
this.reconnectAttempts++;
|
|
289
|
+
|
|
290
|
+
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
291
|
+
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
this.establishConnection();
|
|
294
|
+
}, delay);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Send input to server
|
|
298
|
+
sendInput(data: Uint8Array) {
|
|
299
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
300
|
+
// Input during disconnect is discarded
|
|
301
|
+
// User will notice and retype
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.send({
|
|
306
|
+
type: 'input',
|
|
307
|
+
data: data,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Send resize event
|
|
312
|
+
sendResize(cols: number, rows: number) {
|
|
313
|
+
this.send({
|
|
314
|
+
type: 'resize',
|
|
315
|
+
cols,
|
|
316
|
+
rows,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Reconnect Backoff
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
Attempt 1: wait 1 second
|
|
326
|
+
Attempt 2: wait 2 seconds
|
|
327
|
+
Attempt 3: wait 4 seconds
|
|
328
|
+
Attempt 4: wait 8 seconds
|
|
329
|
+
Attempt 5: wait 16 seconds
|
|
330
|
+
Attempt 6+: wait 30 seconds (max)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
On successful connection, reset attempts to 0.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Server Implementation
|
|
338
|
+
|
|
339
|
+
### Session State with xterm-headless
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { Terminal } from 'xterm-headless';
|
|
343
|
+
|
|
344
|
+
class Session {
|
|
345
|
+
private terminal: Terminal;
|
|
346
|
+
private pty: IPty;
|
|
347
|
+
private scrollback: string[] = [];
|
|
348
|
+
private maxScrollback = 1000;
|
|
349
|
+
|
|
350
|
+
constructor(cols: number, rows: number) {
|
|
351
|
+
this.terminal = new Terminal({ cols, rows });
|
|
352
|
+
this.pty = spawn('bash', [], { cols, rows });
|
|
353
|
+
|
|
354
|
+
// Capture output to both terminal and scrollback
|
|
355
|
+
this.pty.onData((data) => {
|
|
356
|
+
this.terminal.write(data);
|
|
357
|
+
this.appendScrollback(data);
|
|
358
|
+
this.broadcastOutput(data);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getStateSync(): StateSync {
|
|
363
|
+
// Serialize current screen buffer
|
|
364
|
+
const buffer = this.terminal.buffer.active;
|
|
365
|
+
const lines: string[] = [];
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
368
|
+
const line = buffer.getLine(i);
|
|
369
|
+
if (line) {
|
|
370
|
+
lines.push(line.translateToString(true));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
type: 'state-sync',
|
|
376
|
+
screen: this.serializeScreen(),
|
|
377
|
+
cursorX: buffer.cursorX,
|
|
378
|
+
cursorY: buffer.cursorY,
|
|
379
|
+
cols: this.terminal.cols,
|
|
380
|
+
rows: this.terminal.rows,
|
|
381
|
+
scrollback: this.scrollback.join('\n'),
|
|
382
|
+
title: this.pty.process || 'bash',
|
|
383
|
+
cwd: this.getCwd(),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private serializeScreen(): string {
|
|
388
|
+
// Use xterm's serialize addon or manual ANSI construction
|
|
389
|
+
// Returns ANSI-encoded string that recreates the screen
|
|
390
|
+
return serializeTerminal(this.terminal);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private appendScrollback(data: string) {
|
|
394
|
+
// Simple line tracking for scrollback
|
|
395
|
+
const lines = data.split('\n');
|
|
396
|
+
this.scrollback.push(...lines);
|
|
397
|
+
|
|
398
|
+
// Trim to max
|
|
399
|
+
if (this.scrollback.length > this.maxScrollback) {
|
|
400
|
+
this.scrollback = this.scrollback.slice(-this.maxScrollback);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Client Attach Handling
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
class SessionManager {
|
|
410
|
+
private sessions: Map<string, Session> = new Map();
|
|
411
|
+
private clients: Map<string, Set<Client>> = new Map();
|
|
412
|
+
|
|
413
|
+
attachClient(client: Client, sessionId: string) {
|
|
414
|
+
const session = this.sessions.get(sessionId);
|
|
415
|
+
if (!session) {
|
|
416
|
+
client.send({ type: 'error', message: 'Session not found' });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Track this client
|
|
421
|
+
if (!this.clients.has(sessionId)) {
|
|
422
|
+
this.clients.set(sessionId, new Set());
|
|
423
|
+
}
|
|
424
|
+
this.clients.get(sessionId)!.add(client);
|
|
425
|
+
|
|
426
|
+
// Send current state
|
|
427
|
+
client.send(session.getStateSync());
|
|
428
|
+
|
|
429
|
+
// Client is now receiving output stream
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
detachClient(client: Client, sessionId: string) {
|
|
433
|
+
const clients = this.clients.get(sessionId);
|
|
434
|
+
if (clients) {
|
|
435
|
+
clients.delete(client);
|
|
436
|
+
}
|
|
437
|
+
// Session continues running regardless
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
broadcastOutput(sessionId: string, data: Uint8Array) {
|
|
441
|
+
const clients = this.clients.get(sessionId);
|
|
442
|
+
if (!clients) return;
|
|
443
|
+
|
|
444
|
+
for (const client of clients) {
|
|
445
|
+
client.send({ type: 'output', data });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Connection Health
|
|
454
|
+
|
|
455
|
+
### Heartbeat / Keepalive
|
|
456
|
+
|
|
457
|
+
WebSocket connections can silently die. We use ping/pong to detect this:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// Server side
|
|
461
|
+
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
462
|
+
const HEARTBEAT_TIMEOUT = 10000; // 10 seconds to respond
|
|
463
|
+
|
|
464
|
+
class ClientConnection {
|
|
465
|
+
private heartbeatTimer: Timer | null = null;
|
|
466
|
+
private pongReceived = true;
|
|
467
|
+
|
|
468
|
+
startHeartbeat() {
|
|
469
|
+
this.heartbeatTimer = setInterval(() => {
|
|
470
|
+
if (!this.pongReceived) {
|
|
471
|
+
// Client didn't respond to last ping
|
|
472
|
+
this.disconnect('heartbeat timeout');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
this.pongReceived = false;
|
|
477
|
+
this.ws.ping();
|
|
478
|
+
}, HEARTBEAT_INTERVAL);
|
|
479
|
+
|
|
480
|
+
this.ws.on('pong', () => {
|
|
481
|
+
this.pongReceived = true;
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Client-Side Detection
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// Client side
|
|
491
|
+
class ConnectionManager {
|
|
492
|
+
private lastActivity = Date.now();
|
|
493
|
+
private activityCheckInterval: Timer | null = null;
|
|
494
|
+
|
|
495
|
+
startActivityCheck() {
|
|
496
|
+
this.activityCheckInterval = setInterval(() => {
|
|
497
|
+
const idle = Date.now() - this.lastActivity;
|
|
498
|
+
|
|
499
|
+
if (idle > 60000) {
|
|
500
|
+
// No activity for 60 seconds, check connection
|
|
501
|
+
this.sendPing();
|
|
502
|
+
}
|
|
503
|
+
}, 30000);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private handleMessage(event: MessageEvent) {
|
|
507
|
+
this.lastActivity = Date.now();
|
|
508
|
+
// ... process message
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Edge Cases
|
|
516
|
+
|
|
517
|
+
### Resize During Disconnect
|
|
518
|
+
|
|
519
|
+
If the user resizes their terminal while disconnected:
|
|
520
|
+
|
|
521
|
+
1. Client reconnects with new dimensions
|
|
522
|
+
2. Client sends `resize` message
|
|
523
|
+
3. Server resizes PTY and xterm-headless
|
|
524
|
+
4. Server sends fresh state-sync with new dimensions
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
async reconnect() {
|
|
528
|
+
await this.establishConnection();
|
|
529
|
+
|
|
530
|
+
// Send current terminal size
|
|
531
|
+
this.sendResize(this.terminal.cols, this.terminal.rows);
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Session Ended During Disconnect
|
|
536
|
+
|
|
537
|
+
If the session exits while the client was disconnected:
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
handleMessage(msg: Message) {
|
|
541
|
+
switch (msg.type) {
|
|
542
|
+
case 'error':
|
|
543
|
+
if (msg.code === 'SESSION_NOT_FOUND') {
|
|
544
|
+
this.terminal.showSessionEnded();
|
|
545
|
+
this.stopReconnecting();
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case 'exited':
|
|
550
|
+
this.terminal.showExitCode(msg.exitCode);
|
|
551
|
+
this.stopReconnecting();
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Input During Disconnect
|
|
558
|
+
|
|
559
|
+
**We discard it.** User will notice the connection is down and retype.
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
sendInput(data: Uint8Array) {
|
|
563
|
+
if (!this.isConnected()) {
|
|
564
|
+
// Show visual indicator that input isn't going through
|
|
565
|
+
this.terminal.showDisconnectedIndicator();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.send({ type: 'input', data });
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Visual Feedback
|
|
576
|
+
|
|
577
|
+
The client should clearly indicate connection state:
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
581
|
+
│ CONNECTED │
|
|
582
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
583
|
+
│ │ $ npm run dev │ │
|
|
584
|
+
│ │ > ready on http://localhost:3000 │ │
|
|
585
|
+
│ │ █ │ │
|
|
586
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
587
|
+
│ [●] │
|
|
588
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
589
|
+
|
|
590
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
591
|
+
│ DISCONNECTED │
|
|
592
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
593
|
+
│ │ $ npm run dev │ │
|
|
594
|
+
│ │ > ready on http://localhost:3000 │ │
|
|
595
|
+
│ │ █ │ │
|
|
596
|
+
│ │ │ │
|
|
597
|
+
│ │ ┌─────────────────────────────────┐ │ │
|
|
598
|
+
│ │ │ ⚠ Connection lost │ │ │
|
|
599
|
+
│ │ │ Reconnecting in 4s... │ │ │
|
|
600
|
+
│ │ └─────────────────────────────────┘ │ │
|
|
601
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
602
|
+
│ [○] │
|
|
603
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Summary
|
|
609
|
+
|
|
610
|
+
| Scenario | Handling |
|
|
611
|
+
|----------|----------|
|
|
612
|
+
| **Network blip** | Auto-reconnect with backoff, state sync |
|
|
613
|
+
| **Long disconnect** | Same - state sync gives current screen + scrollback |
|
|
614
|
+
| **Input during disconnect** | Discarded - user will retype |
|
|
615
|
+
| **Resize during disconnect** | Applied on reconnect |
|
|
616
|
+
| **Session died** | Error message, stop reconnecting |
|
|
617
|
+
| **Dead connection** | Heartbeat detection, trigger reconnect |
|
|
618
|
+
|
|
619
|
+
**The core principle:** The server always knows the truth. Clients just ask "what does the screen look like now?" and render it.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
*Last updated: 2025-01*
|