gitspace 0.2.0-rc.2 → 0.2.0-rc.21

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