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.
Files changed (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. 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*