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,23 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { defineConfig, globalIgnores } from 'eslint/config'
7
-
8
- export default defineConfig([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['**/*.{ts,tsx}'],
12
- extends: [
13
- js.configs.recommended,
14
- tseslint.configs.recommended,
15
- reactHooks.configs.flat.recommended,
16
- reactRefresh.configs.vite,
17
- ],
18
- languageOptions: {
19
- ecmaVersion: 2020,
20
- globals: globals.browser,
21
- },
22
- },
23
- ])
@@ -1,16 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
7
- <meta name="apple-mobile-web-app-capable" content="yes" />
8
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
- <meta name="format-detection" content="telephone=no" />
10
- <title>Spaces Terminal</title>
11
- </head>
12
- <body>
13
- <div id="root"></div>
14
- <script type="module" src="/src/main.tsx"></script>
15
- </body>
16
- </html>
@@ -1,37 +0,0 @@
1
- {
2
- "name": "web",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc -b && vite build",
9
- "lint": "eslint .",
10
- "preview": "vite preview"
11
- },
12
- "dependencies": {
13
- "@noble/ciphers": "^2.1.1",
14
- "@noble/curves": "^2.0.1",
15
- "@noble/hashes": "^2.0.1",
16
- "ghostty-web": "^0.4.0",
17
- "react": "^19.2.0",
18
- "react-dom": "^19.2.0",
19
- "react-router-dom": "^7.11.0"
20
- },
21
- "devDependencies": {
22
- "@eslint/js": "^9.39.1",
23
- "@tailwindcss/vite": "^4.1.18",
24
- "@types/node": "^25.0.3",
25
- "@types/react": "^19.2.5",
26
- "@types/react-dom": "^19.2.3",
27
- "@vitejs/plugin-react": "^5.1.1",
28
- "eslint": "^9.39.1",
29
- "eslint-plugin-react-hooks": "^7.0.1",
30
- "eslint-plugin-react-refresh": "^0.4.24",
31
- "globals": "^16.5.0",
32
- "tailwindcss": "^4.1.18",
33
- "typescript": "~5.9.3",
34
- "typescript-eslint": "^8.46.4",
35
- "vite": "^7.2.4"
36
- }
37
- }
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -1,604 +0,0 @@
1
- /** @jsxImportSource react */
2
- import { useState, useEffect } from "react";
3
- import { Terminal } from "./components/Terminal";
4
- import { useTerminal } from "./hooks/useTerminal";
5
- import { useRelayConnection } from "./hooks/useRelayConnection";
6
- import { parseInviteFromHash } from "./lib/invite";
7
-
8
- // Import shared components and hooks
9
- import {
10
- useMachineList,
11
- useSpacesBrowser,
12
- useFlow,
13
- getDefaultShortcuts,
14
- type MachineInfo,
15
- } from "../../shared/components/index.js";
16
- import { MachineListWeb } from "../../shared/components/MachineList.web.js";
17
- import { SpacesBrowserWeb } from "../../shared/components/SpacesBrowser.web.js";
18
- import { FlowWeb } from "../../shared/components/Flow.web.js";
19
- import { useInbox } from "../../shared/components/Inbox.js";
20
- import { InboxWeb } from "../../shared/components/Inbox.web.js";
21
-
22
- type View = "machines" | "terminal";
23
-
24
- export default function App() {
25
- const [view, setView] = useState<View>("machines");
26
- const [selectedMachine, setSelectedMachine] = useState<MachineInfo | null>(null);
27
- const [showInbox, setShowInbox] = useState(false);
28
- const [copied, setCopied] = useState(false);
29
-
30
- // Invite params from URL
31
- const [inviteParams, setInviteParams] = useState<{
32
- machineId?: string;
33
- inviteId?: string;
34
- inviteToken?: string;
35
- } | null>(null);
36
-
37
- // Relay connection (for machine list)
38
- const relay = useRelayConnection();
39
-
40
- // Terminal connection (for PTY)
41
- const terminal = useTerminal();
42
-
43
- // Flow/Modal system
44
- const flow = useFlow({
45
- onError: (error) => console.error('Flow error:', error),
46
- });
47
-
48
- // Parse invite from URL hash on load
49
- useEffect(() => {
50
- const hash = window.location.hash;
51
- if (hash.startsWith("#invite=")) {
52
- parseInviteFromHash(hash).then((invite) => {
53
- if (invite) {
54
- setInviteParams({
55
- machineId: invite.machineId,
56
- inviteId: invite.inviteId,
57
- inviteToken: invite.inviteToken,
58
- });
59
- }
60
- });
61
- }
62
- }, []);
63
-
64
- // Auto-connect on load (no token required for personal relays)
65
- useEffect(() => {
66
- if (relay.status === "disconnected") {
67
- relay.connect();
68
- }
69
- }, []);
70
-
71
- // Copy access command to clipboard
72
- const copyAccessCommand = async () => {
73
- if (relay.publicKey) {
74
- const command = `gssh access add "${relay.publicKey}"`;
75
- await navigator.clipboard.writeText(command);
76
- setCopied(true);
77
- setTimeout(() => setCopied(false), 2000);
78
- }
79
- };
80
-
81
- // Handle machine selection - go directly to terminal/workspaces view
82
- const handleMachineConnect = async (machine: MachineInfo) => {
83
- if (!machine.online) return;
84
-
85
- // Get WebSocket and identity from relay connection
86
- const ws = relay.getWebSocket();
87
- const identity = relay.identity;
88
- if (!ws || !identity) {
89
- console.error("No WebSocket or identity available");
90
- return;
91
- }
92
-
93
- setSelectedMachine(machine);
94
- setView("terminal");
95
-
96
- // Connect to the machine using existing WebSocket (no new connection needed)
97
- await terminal.connect({
98
- ws,
99
- identity,
100
- machineId: machine.machineId,
101
- inviteId: inviteParams?.inviteId,
102
- inviteToken: inviteParams?.inviteToken,
103
- });
104
- };
105
-
106
- // Handle back to machine list
107
- const handleBackToMachines = () => {
108
- terminal.disconnect();
109
- setSelectedMachine(null);
110
- setView("machines");
111
- };
112
-
113
- // Handle full disconnect (just refresh the page for simplicity)
114
- const handleDisconnect = () => {
115
- terminal.disconnect();
116
- relay.disconnect();
117
- setSelectedMachine(null);
118
- setView("machines");
119
- // Reconnect automatically
120
- relay.connect();
121
- };
122
-
123
- // ========== Shared Hooks ==========
124
-
125
- // Machine list hook - convert relay machines to shared MachineInfo format
126
- const machineListProps = useMachineList({
127
- machines: relay.machines,
128
- status: relay.status,
129
- error: relay.error,
130
- publicKey: relay.publicKey,
131
- onConnect: handleMachineConnect,
132
- onRefresh: relay.refreshMachines,
133
- });
134
-
135
- // Handle attach session - show modal for new sessions
136
- const handleAttachSession = (params: { sessionId?: string; workspaceId?: string }) => {
137
- console.log('[App] handleAttachSession called with:', params);
138
- if (params.sessionId) {
139
- // Existing session - attach directly
140
- console.log('[App] Attaching to existing session:', params.sessionId);
141
- terminal.attachSession(params);
142
- } else if (params.workspaceId) {
143
- // New session - show input modal for name
144
- flow.showInput({
145
- title: 'New Session',
146
- label: 'Session name (optional):',
147
- placeholder: 'Leave empty for auto-generated name',
148
- onSubmit: (name) => {
149
- terminal.attachSession({
150
- workspaceId: params.workspaceId,
151
- sessionName: name || undefined
152
- });
153
- },
154
- });
155
- }
156
- };
157
-
158
- // Spaces browser hook
159
- const spacesBrowserProps = useSpacesBrowser({
160
- workspaces: terminal.workspaces,
161
- sessions: terminal.sessions,
162
- onRequestSessions: terminal.requestSessions,
163
- onAttachSession: handleAttachSession,
164
- onRefresh: terminal.requestWorkspaces,
165
- onRefreshSessions: (workspaceIds) => {
166
- workspaceIds.forEach(id => terminal.requestSessions(id));
167
- },
168
- onBack: handleBackToMachines,
169
- machineName: selectedMachine?.label || selectedMachine?.machineId,
170
- });
171
-
172
- // Inbox hook
173
- const inboxProps = useInbox({
174
- items: terminal.inbox,
175
- unreadCount: terminal.inboxUnreadCount,
176
- onClearItem: async (id) => terminal.clearInboxItem(id),
177
- onClearAll: async () => terminal.clearInboxItem(),
178
- onMarkRead: async (id) => terminal.markInboxItemRead(id),
179
- onAttachSession: async (sessionId) => {
180
- setShowInbox(false);
181
- terminal.attachSession({ sessionId });
182
- },
183
- onClose: () => setShowInbox(false),
184
- });
185
-
186
- // Request workspaces when connection is established and view is "terminal"
187
- useEffect(() => {
188
- if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
189
- terminal.requestWorkspaces();
190
- }
191
- }, [view, terminal.status, terminal.mode, terminal.requestWorkspaces]);
192
-
193
- // ========== Keyboard Handlers ==========
194
-
195
- // Machine list keyboard navigation
196
- useEffect(() => {
197
- if (view !== "machines") return;
198
-
199
- const handleKeyDown = (e: KeyboardEvent) => {
200
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
201
- return;
202
- }
203
-
204
- const key = e.key;
205
- if (key === "ArrowUp" || key === "k") {
206
- e.preventDefault();
207
- machineListProps.moveUp();
208
- } else if (key === "ArrowDown" || key === "j") {
209
- e.preventDefault();
210
- machineListProps.moveDown();
211
- } else if (key === "Enter") {
212
- e.preventDefault();
213
- machineListProps.connectSelected();
214
- } else if (key === "r") {
215
- e.preventDefault();
216
- machineListProps.refresh();
217
- } else if (key === "c") {
218
- e.preventDefault();
219
- machineListProps.copyPublicKey();
220
- } else if (key === "?") {
221
- e.preventDefault();
222
- flow.showHelp(getDefaultShortcuts());
223
- }
224
- };
225
-
226
- window.addEventListener("keydown", handleKeyDown);
227
- return () => window.removeEventListener("keydown", handleKeyDown);
228
- }, [view, machineListProps, flow]);
229
-
230
- // Inbox keyboard navigation
231
- useEffect(() => {
232
- if (!showInbox) return;
233
-
234
- const handleKeyDown = (e: KeyboardEvent) => {
235
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
236
- return;
237
- }
238
-
239
- const key = e.key;
240
- if (key === "ArrowUp" || key === "k") {
241
- e.preventDefault();
242
- inboxProps.moveUp();
243
- } else if (key === "ArrowDown" || key === "j") {
244
- e.preventDefault();
245
- inboxProps.moveDown();
246
- } else if (key === "Enter") {
247
- e.preventDefault();
248
- if (inboxProps.isViewingThread) {
249
- // In thread view, attach to session
250
- inboxProps.attachToSession();
251
- } else {
252
- // In list view, open thread
253
- inboxProps.openThread();
254
- }
255
- } else if (key === "Escape" || key === "q") {
256
- e.preventDefault();
257
- if (inboxProps.isViewingThread) {
258
- inboxProps.closeThread();
259
- } else {
260
- setShowInbox(false);
261
- }
262
- } else if (key === "x") {
263
- e.preventDefault();
264
- if (inboxProps.isViewingThread) {
265
- inboxProps.deleteThread();
266
- } else {
267
- inboxProps.deleteSelected();
268
- }
269
- } else if (key === "c") {
270
- e.preventDefault();
271
- inboxProps.clearAll();
272
- } else if (key === "a") {
273
- e.preventDefault();
274
- inboxProps.attachToSession();
275
- }
276
- };
277
-
278
- window.addEventListener("keydown", handleKeyDown);
279
- return () => window.removeEventListener("keydown", handleKeyDown);
280
- }, [showInbox, inboxProps.moveUp, inboxProps.moveDown, inboxProps.openThread, inboxProps.closeThread, inboxProps.deleteSelected, inboxProps.deleteThread, inboxProps.clearAll, inboxProps.attachToSession, inboxProps.isViewingThread]);
281
-
282
- // Spaces browser keyboard navigation
283
- useEffect(() => {
284
- if (view !== "terminal" || terminal.status !== "established" || terminal.mode !== "browsing" || showInbox) {
285
- return;
286
- }
287
-
288
- const handleKeyDown = (e: KeyboardEvent) => {
289
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
290
- return;
291
- }
292
-
293
- const key = e.key;
294
- if (key === "ArrowUp" || key === "k") {
295
- e.preventDefault();
296
- spacesBrowserProps.moveUp();
297
- } else if (key === "ArrowDown" || key === "j") {
298
- e.preventDefault();
299
- spacesBrowserProps.moveDown();
300
- } else if (key === "Enter") {
301
- e.preventDefault();
302
- spacesBrowserProps.activateSelected();
303
- } else if (key === "n") {
304
- // New session - uses same flow as clicking "+ New Session"
305
- e.preventDefault();
306
- spacesBrowserProps.createNewSession();
307
- } else if (key === "r") {
308
- e.preventDefault();
309
- spacesBrowserProps.refresh();
310
- } else if (key === "Escape" || key === "q") {
311
- e.preventDefault();
312
- spacesBrowserProps.back();
313
- } else if (key === "?") {
314
- e.preventDefault();
315
- flow.showHelp(getDefaultShortcuts());
316
- } else if (key === "x") {
317
- // Kill session
318
- e.preventDefault();
319
- const selected = spacesBrowserProps.selectedItem;
320
- if (selected?.type === 'session') {
321
- flow.showConfirm({
322
- title: 'Kill Session',
323
- message: `Kill session "${selected.session.name}"?`,
324
- variant: 'warning',
325
- confirmLabel: 'Kill',
326
- onConfirm: () => {
327
- terminal.killSession(selected.session.id);
328
- },
329
- });
330
- }
331
- } else if (key === "d") {
332
- // Delete workspace - require typing name to confirm
333
- e.preventDefault();
334
- const selected = spacesBrowserProps.selectedItem;
335
- if (selected?.type === 'workspace') {
336
- const sessionCount = selected.workspace.sessionCount || 0;
337
- flow.showConfirmTyped({
338
- title: 'Delete Workspace',
339
- message: `Are you sure you want to delete workspace "${selected.workspace.name}"?`,
340
- confirmText: selected.workspace.name,
341
- warning: sessionCount > 0 ? `This will kill ${sessionCount} active session(s)!` : undefined,
342
- onConfirm: () => {
343
- terminal.deleteWorkspace(selected.workspace.projectName, selected.workspace.id);
344
- },
345
- });
346
- }
347
- } else if (key === "i") {
348
- // Open inbox
349
- e.preventDefault();
350
- terminal.requestInbox();
351
- setShowInbox(true);
352
- }
353
- };
354
-
355
- window.addEventListener("keydown", handleKeyDown);
356
- return () => window.removeEventListener("keydown", handleKeyDown);
357
- }, [view, terminal.status, terminal.mode, spacesBrowserProps, flow]);
358
-
359
- // Attached terminal mode keyboard handler (Ctrl+Esc to detach)
360
- useEffect(() => {
361
- if (view !== "terminal" || terminal.status !== "established" || terminal.mode !== "attached") {
362
- return;
363
- }
364
-
365
- const handleKeyDown = (e: KeyboardEvent) => {
366
- // Ctrl+Esc to detach from session
367
- if (e.ctrlKey && e.key === "Escape") {
368
- e.preventDefault();
369
- terminal.detachSession();
370
- }
371
- };
372
-
373
- window.addEventListener("keydown", handleKeyDown);
374
- return () => window.removeEventListener("keydown", handleKeyDown);
375
- }, [view, terminal.status, terminal.mode, terminal.detachSession]);
376
-
377
- // ========== Spaces Browser View (browsing mode) ==========
378
- if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
379
- // Show inbox if open
380
- if (showInbox) {
381
- return (
382
- <>
383
- <InboxWeb {...inboxProps} />
384
- <FlowWeb flow={flow} />
385
- </>
386
- );
387
- }
388
-
389
- return (
390
- <>
391
- <div className="h-screen w-screen flex flex-col bg-gray-900">
392
- <div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-b border-gray-700 min-h-[52px] gap-2">
393
- <div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
394
- <button
395
- onClick={handleBackToMachines}
396
- className="text-sm text-gray-400 hover:text-white active:text-blue-400 py-2 pr-2 -ml-2 min-h-[44px] flex items-center flex-shrink-0"
397
- >
398
- ← <span className="hidden sm:inline ml-1">Machines</span>
399
- </button>
400
- <div className="text-sm text-gray-400 truncate hidden sm:block">
401
- <span className="text-green-400">●</span>{" "}
402
- {selectedMachine?.label || selectedMachine?.machineId}
403
- </div>
404
- </div>
405
- <div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
406
- <button
407
- onClick={() => {
408
- terminal.requestInbox();
409
- setShowInbox(true);
410
- }}
411
- className="text-sm text-gray-400 hover:text-white active:text-blue-400 flex items-center gap-1 py-2 px-2 min-h-[44px]"
412
- >
413
- <span className="hidden sm:inline text-xs text-gray-500">[i]</span>
414
- <span>Inbox</span>
415
- {terminal.inboxUnreadCount > 0 && (
416
- <span className="ml-1 px-1.5 py-0.5 text-xs bg-blue-600 rounded-full text-white">
417
- {terminal.inboxUnreadCount}
418
- </span>
419
- )}
420
- </button>
421
- <button
422
- onClick={handleDisconnect}
423
- className="px-3 py-2 text-sm bg-red-600 hover:bg-red-700 active:bg-red-800 rounded text-white min-h-[44px]"
424
- >
425
- <span className="hidden sm:inline">Disconnect</span>
426
- <span className="sm:hidden">×</span>
427
- </button>
428
- </div>
429
- </div>
430
- <div className="flex-1 overflow-hidden">
431
- <SpacesBrowserWeb {...spacesBrowserProps} />
432
- </div>
433
- </div>
434
- <FlowWeb flow={flow} />
435
- </>
436
- );
437
- }
438
-
439
- // ========== Terminal View (attached mode) ==========
440
- if (view === "terminal" && terminal.status === "established" && terminal.mode === "attached") {
441
- return (
442
- <div className="h-screen w-screen flex flex-col bg-gray-900">
443
- <div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-b border-gray-700 min-h-[52px] gap-2">
444
- <div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
445
- <button
446
- onClick={terminal.detachSession}
447
- className="text-sm text-gray-400 hover:text-white active:text-blue-400 py-2 pr-2 -ml-2 min-h-[44px] flex items-center flex-shrink-0"
448
- >
449
- ← <span className="hidden sm:inline ml-1">Workspaces</span>
450
- </button>
451
- <div className="text-sm text-gray-400 truncate">
452
- <span className="text-green-400">●</span>{" "}
453
- <span className="hidden sm:inline">{selectedMachine?.label || selectedMachine?.machineId}</span>
454
- {terminal.attachedSessionName && (
455
- <span className="text-gray-300">
456
- <span className="hidden sm:inline text-gray-500 mx-1">/</span>
457
- {terminal.attachedSessionName.split(':').pop()}
458
- </span>
459
- )}
460
- </div>
461
- </div>
462
- <div className="flex items-center gap-2 flex-shrink-0">
463
- <span className="text-xs text-gray-500 hidden sm:inline">Ctrl+Esc</span>
464
- <button
465
- onClick={terminal.detachSession}
466
- className="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 active:bg-gray-500 rounded text-white min-h-[44px]"
467
- >
468
- Detach
469
- </button>
470
- </div>
471
- </div>
472
- <div className="flex-1">
473
- <Terminal
474
- onData={terminal.send}
475
- setWriteCallback={terminal.setWriteCallback}
476
- onResize={terminal.resize}
477
- />
478
- </div>
479
- </div>
480
- );
481
- }
482
-
483
- // ========== Terminal Connecting View ==========
484
- if (view === "terminal") {
485
- const statusMessage = {
486
- disconnected: "Disconnected",
487
- connecting: "Connecting to relay...",
488
- connected: "Connected, authenticating...",
489
- handshaking: "Establishing secure connection...",
490
- established: "Connected!",
491
- error: "Connection failed",
492
- }[terminal.status];
493
-
494
- return (
495
- <div className="h-screen w-screen flex flex-col items-center justify-center bg-gray-900 px-4">
496
- <div className="text-center">
497
- <div className="text-lg text-white mb-2 break-words">
498
- Connecting to {selectedMachine?.label || selectedMachine?.machineId}
499
- </div>
500
- <div className="text-sm text-gray-400">{statusMessage}</div>
501
- {terminal.status === "error" && (
502
- <button
503
- onClick={handleBackToMachines}
504
- className="mt-4 px-6 py-3 text-base bg-gray-700 hover:bg-gray-600 active:bg-gray-500 rounded-lg text-white min-h-[48px]"
505
- >
506
- Back to Machines
507
- </button>
508
- )}
509
- </div>
510
- </div>
511
- );
512
- }
513
-
514
- // ========== Machine List View ==========
515
- // This is now the main/default view - shows machines and your identity
516
- return (
517
- <>
518
- <div className="h-screen w-screen flex flex-col bg-gray-900">
519
- {/* Header with identity info */}
520
- <div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
521
- <div className="max-w-2xl mx-auto">
522
- {/* Connection status */}
523
- <div className="flex items-center justify-between mb-3">
524
- <div className="flex items-center gap-2">
525
- <span className={`w-2 h-2 rounded-full ${
526
- relay.status === "connected" ? "bg-green-400" :
527
- relay.status === "connecting" ? "bg-yellow-400 animate-pulse" :
528
- "bg-red-400"
529
- }`} />
530
- <span className="text-sm text-gray-400">
531
- {relay.status === "connected" ? "Connected" :
532
- relay.status === "connecting" ? "Connecting..." :
533
- "Disconnected"}
534
- </span>
535
- </div>
536
- <button
537
- onClick={relay.refreshMachines}
538
- className="text-xs text-gray-500 hover:text-white px-2 py-1"
539
- >
540
- Refresh
541
- </button>
542
- </div>
543
-
544
- {/* Your identity - prominent display */}
545
- {relay.publicKey && (
546
- <div className="bg-gray-900 rounded-lg p-3">
547
- <div className="flex items-center justify-between mb-2">
548
- <span className="text-xs text-gray-400">Your Browser Identity</span>
549
- </div>
550
- <code className="block text-xs text-green-400 break-all font-mono leading-relaxed mb-3">
551
- {relay.publicKey}
552
- </code>
553
- <p className="text-xs text-gray-500 mb-2">
554
- To get access, have the machine owner run:
555
- </p>
556
- <div className="flex items-center gap-2">
557
- <code className="flex-1 text-xs text-gray-300 bg-gray-800 px-2 py-2 rounded font-mono overflow-x-auto">
558
- gssh access add "{relay.publicKey.slice(0, 20)}..."
559
- </code>
560
- <button
561
- onClick={copyAccessCommand}
562
- className="text-xs text-blue-400 hover:text-blue-300 bg-gray-800 px-3 py-2 rounded whitespace-nowrap"
563
- >
564
- {copied ? "Copied!" : "Copy Command"}
565
- </button>
566
- </div>
567
- </div>
568
- )}
569
- </div>
570
- </div>
571
-
572
- {/* Machine list */}
573
- <div className="flex-1 overflow-auto">
574
- {relay.status === "connecting" ? (
575
- <div className="flex items-center justify-center h-full">
576
- <div className="text-gray-400">Connecting to relay...</div>
577
- </div>
578
- ) : relay.machines.length === 0 ? (
579
- <div className="flex items-center justify-center h-full p-4">
580
- <div className="text-center max-w-md">
581
- <div className="text-gray-400 mb-2">No machines available</div>
582
- <p className="text-sm text-gray-500">
583
- {relay.status === "connected"
584
- ? "The machine may not be online. Check if 'gssh serve' is running."
585
- : "Unable to connect to relay."}
586
- </p>
587
- </div>
588
- </div>
589
- ) : (
590
- <MachineListWeb {...machineListProps} />
591
- )}
592
- </div>
593
-
594
- {/* Footer */}
595
- <div className="bg-gray-800 px-4 py-2 border-t border-gray-700">
596
- <p className="text-xs text-gray-500 text-center">
597
- End-to-end encrypted via X3DH
598
- </p>
599
- </div>
600
- </div>
601
- <FlowWeb flow={flow} />
602
- </>
603
- );
604
- }