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,221 @@
1
+ /** @jsxImportSource react */
2
+ /**
3
+ * SpacesBrowser - Web Display Component
4
+ *
5
+ * Dumb presentational component for web.
6
+ * Receives all state and actions from useSpacesBrowser hook.
7
+ */
8
+
9
+ import type { UseSpacesBrowserReturn } from './SpacesBrowser.js';
10
+ import { formatTime } from './SpacesBrowser.js';
11
+
12
+ // ============================================================================
13
+ // Component
14
+ // ============================================================================
15
+
16
+ export function SpacesBrowserWeb(props: UseSpacesBrowserReturn) {
17
+ const {
18
+ items,
19
+ machineName,
20
+ isEmpty,
21
+ selectIndex,
22
+ toggleWorkspace,
23
+ attachSession,
24
+ refresh,
25
+ back,
26
+ } = props;
27
+
28
+ // Empty state
29
+ if (isEmpty) {
30
+ return (
31
+ <div className="h-screen flex flex-col bg-gray-900">
32
+ <Header machineName={machineName} onBack={back} onRefresh={refresh} />
33
+ <div className="flex-1 flex flex-col items-center justify-center text-gray-400 px-4">
34
+ <div className="text-lg mb-2 text-center">No workspaces found</div>
35
+ <div className="text-sm text-gray-500 text-center">
36
+ Create workspaces with <code className="text-green-400">gssh add</code>
37
+ </div>
38
+ </div>
39
+ <Footer />
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div className="h-screen flex flex-col bg-gray-900">
46
+ <Header machineName={machineName} onBack={back} onRefresh={refresh} />
47
+
48
+ {/* Tree list */}
49
+ <div className="flex-1 overflow-y-auto">
50
+ {items.map((item) => {
51
+ const { isSelected, index } = item;
52
+
53
+ if (item.type === 'project') {
54
+ return (
55
+ <div
56
+ key={`project-${item.name}`}
57
+ className="px-4 py-3 text-xs text-gray-500 uppercase tracking-wide bg-gray-800 border-b border-gray-700 min-h-[44px] flex items-center"
58
+ >
59
+ {item.name} ({item.workspaceCount})
60
+ </div>
61
+ );
62
+ }
63
+
64
+ if (item.type === 'workspace') {
65
+ const ws = item.workspace;
66
+ return (
67
+ <div
68
+ key={`ws-${ws.id}`}
69
+ onClick={(e) => {
70
+ e.stopPropagation();
71
+ console.log('[SpacesBrowser] Workspace clicked:', ws.id, ws.name);
72
+ selectIndex(index);
73
+ toggleWorkspace(ws.id);
74
+ }}
75
+ className={`
76
+ px-4 py-4 cursor-pointer border-b border-gray-800 flex items-center justify-between min-h-[56px]
77
+ ${isSelected ? 'bg-gray-700 border-l-4 border-l-blue-500' : 'hover:bg-gray-800 active:bg-gray-700'}
78
+ ${ws.isStale ? 'opacity-60' : ''}
79
+ `}
80
+ >
81
+ <div className="flex items-center gap-3 min-w-0 flex-1">
82
+ <span className="text-gray-500 w-5 flex-shrink-0 text-center">
83
+ {item.expanded ? '▼' : '▶'}
84
+ </span>
85
+ <div className="min-w-0 flex-1">
86
+ <div className="text-white font-medium truncate">{ws.name}</div>
87
+ {ws.branch && (
88
+ <div className="text-xs text-gray-500 truncate">
89
+ <span className="text-purple-400">{ws.branch}</span>
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+ <div className="text-right flex-shrink-0 ml-2">
95
+ {ws.sessionCount > 0 && (
96
+ <span className="text-xs px-2 py-1 rounded bg-green-900 text-green-300">
97
+ {ws.sessionCount}
98
+ </span>
99
+ )}
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ if (item.type === 'session') {
106
+ const session = item.session;
107
+ return (
108
+ <div
109
+ key={`session-${session.id}`}
110
+ onClick={(e) => {
111
+ e.stopPropagation();
112
+ console.log('[SpacesBrowser] Session clicked:', session.id, session.name);
113
+ selectIndex(index);
114
+ attachSession({ sessionId: session.id });
115
+ }}
116
+ className={`
117
+ pl-10 sm:pl-12 pr-4 py-3 cursor-pointer border-b border-gray-800 flex items-center justify-between min-h-[52px]
118
+ ${isSelected ? 'bg-gray-700 border-l-4 border-l-blue-500' : 'hover:bg-gray-800 active:bg-gray-700'}
119
+ `}
120
+ >
121
+ <div className="flex items-center gap-3 min-w-0 flex-1">
122
+ <span className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${session.attached ? 'bg-yellow-500' : 'bg-green-500'}`} />
123
+ <div className="min-w-0 flex-1">
124
+ <span className="text-gray-300 truncate block">{session.name.split(':').pop()}</span>
125
+ {session.processTitle && (
126
+ <span className="text-xs text-yellow-400 truncate block">{session.processTitle}</span>
127
+ )}
128
+ </div>
129
+ </div>
130
+ <div className="text-xs text-gray-500 flex-shrink-0 ml-2">
131
+ {session.attached ? 'attached' : formatTime(session.createdAt)}
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ if (item.type === 'new-session') {
138
+ return (
139
+ <div
140
+ key={`new-${item.workspaceId}`}
141
+ onClick={(e) => {
142
+ e.stopPropagation();
143
+ console.log('[SpacesBrowser] New session clicked for workspace:', item.workspaceId);
144
+ selectIndex(index);
145
+ attachSession({ workspaceId: item.workspaceId });
146
+ }}
147
+ className={`
148
+ pl-10 sm:pl-12 pr-4 py-3 cursor-pointer border-b border-gray-800 min-h-[48px] flex items-center
149
+ ${isSelected ? 'bg-gray-700 border-l-4 border-l-blue-500' : 'hover:bg-gray-800 active:bg-gray-700'}
150
+ `}
151
+ >
152
+ <span className="text-blue-400">+ New Session</span>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ return null;
158
+ })}
159
+ </div>
160
+
161
+ <Footer />
162
+ </div>
163
+ );
164
+ }
165
+
166
+ // ============================================================================
167
+ // Subcomponents
168
+ // ============================================================================
169
+
170
+ function Header({
171
+ machineName,
172
+ onBack,
173
+ onRefresh,
174
+ }: {
175
+ machineName: string | null;
176
+ onBack: () => void;
177
+ onRefresh: () => void;
178
+ }) {
179
+ return (
180
+ <div className="bg-gray-800 px-4 py-3 flex items-center justify-between border-b border-gray-700 min-h-[52px]">
181
+ <div className="flex items-center gap-3 min-w-0 flex-1">
182
+ <button
183
+ onClick={onBack}
184
+ 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"
185
+ >
186
+ ← <span className="hidden sm:inline ml-1">Back</span>
187
+ </button>
188
+ <div className="text-white font-medium truncate">
189
+ {machineName || 'Workspaces'}
190
+ </div>
191
+ </div>
192
+ <button
193
+ onClick={onRefresh}
194
+ className="text-sm text-gray-400 hover:text-white active:text-blue-400 py-2 pl-2 -mr-2 min-h-[44px] flex items-center flex-shrink-0"
195
+ >
196
+ Refresh
197
+ </button>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function Footer() {
203
+ return (
204
+ <div className="bg-gray-800 px-4 py-2 border-t border-gray-700 safe-bottom">
205
+ {/* Desktop keyboard hints */}
206
+ <div className="hidden sm:flex gap-4 text-xs text-gray-500 flex-wrap">
207
+ <span>↑↓ Navigate</span>
208
+ <span>Enter Select</span>
209
+ <span>n New</span>
210
+ <span>x Kill</span>
211
+ <span>d Delete</span>
212
+ <span>r Refresh</span>
213
+ <span>Esc Back</span>
214
+ </div>
215
+ {/* Mobile hint */}
216
+ <div className="sm:hidden text-xs text-gray-500 text-center">
217
+ Tap to expand • Tap session to attach
218
+ </div>
219
+ </div>
220
+ );
221
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared components exports
3
+ *
4
+ * NOTE: Web components (*.web.js) are NOT exported here because they
5
+ * depend on react-dom which is not available in CLI/TUI context.
6
+ * Import web components directly from their files in the web project.
7
+ *
8
+ * TUI components are also not exported here to avoid @opentui/core deps.
9
+ * Import TUI components directly from their files in the TUI project.
10
+ */
11
+
12
+ // MachineList
13
+ export type {
14
+ ConnectionStatus,
15
+ MachineInfo,
16
+ UseMachineListProps,
17
+ MachineListItem,
18
+ UseMachineListReturn,
19
+ } from './MachineList.js';
20
+
21
+ export {
22
+ useMachineList,
23
+ formatLastSeen,
24
+ getStatusColor,
25
+ getMachineLabel,
26
+ } from './MachineList.js';
27
+
28
+ // SpacesBrowser
29
+ export type {
30
+ WorkspaceInfo,
31
+ SessionInfo,
32
+ TreeItem,
33
+ TreeItemWithState,
34
+ UseSpacesBrowserProps,
35
+ UseSpacesBrowserReturn,
36
+ } from './SpacesBrowser.js';
37
+
38
+ export {
39
+ useSpacesBrowser,
40
+ formatTime,
41
+ } from './SpacesBrowser.js';
42
+
43
+ // Inbox
44
+ export type {
45
+ InboxItemType,
46
+ InboxItem,
47
+ ParsedSessionName,
48
+ SessionGroup,
49
+ WorkspaceGroup,
50
+ ProjectGroup,
51
+ InboxDisplayItem,
52
+ UseInboxProps,
53
+ UseInboxReturn,
54
+ } from './Inbox.js';
55
+
56
+ export {
57
+ useInbox,
58
+ parseSessionName,
59
+ getInboxIcon,
60
+ getInboxTypeLabel,
61
+ formatTimeAgo,
62
+ } from './Inbox.js';
63
+
64
+ // ProjectList
65
+ export type {
66
+ ProjectInfo,
67
+ ProjectListItem,
68
+ UseProjectListProps,
69
+ UseProjectListReturn,
70
+ } from './ProjectList.js';
71
+
72
+ export {
73
+ useProjectList,
74
+ getProjectDisplayName,
75
+ getShortRepoName,
76
+ formatWorkspaceCount,
77
+ } from './ProjectList.js';
78
+
79
+ // Flow (Modal System)
80
+ export type {
81
+ FlowNone,
82
+ FlowMessage,
83
+ FlowLoading,
84
+ FlowHelp,
85
+ FlowConfirm,
86
+ FlowConfirmTyped,
87
+ FlowInput,
88
+ FlowSelect,
89
+ FlowWizardStep,
90
+ FlowWizard,
91
+ FlowState,
92
+ UseFlowProps,
93
+ UseFlowReturn,
94
+ } from './Flow.js';
95
+
96
+ export {
97
+ useFlow,
98
+ getDefaultShortcuts,
99
+ isFlowInput,
100
+ isFlowConfirmTyped,
101
+ isFlowWizard,
102
+ hasInputValue,
103
+ } from './Flow.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hook exports
3
+ */
4
+
5
+ export type {
6
+ NavigationState,
7
+ NavigationAction,
8
+ } from './useNavigation.js';
9
+
10
+ export {
11
+ createInitialNavigationState,
12
+ navigationReducer,
13
+ getCurrentMachineId,
14
+ getCurrentProjectName,
15
+ canGoBack,
16
+ } from './useNavigation.js';
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Navigation State Hook
3
+ *
4
+ * Manages navigation between screens (machines, projects, workspaces, sessions).
5
+ * Platform-agnostic - works with both TUI and Web.
6
+ */
7
+
8
+ import type { NavigationLocation, PanelFocus } from '../types.js';
9
+
10
+ /**
11
+ * Navigation state
12
+ */
13
+ export interface NavigationState {
14
+ /** Current location in the app */
15
+ location: NavigationLocation;
16
+ /** Which panel has focus */
17
+ focus: PanelFocus;
18
+ /** History stack for back navigation */
19
+ history: NavigationLocation[];
20
+ /** Selected indices for each panel */
21
+ selectedIndices: {
22
+ machines: number;
23
+ projects: number;
24
+ workspaces: number;
25
+ };
26
+ /** Expanded items (e.g., workspaces showing sessions) */
27
+ expanded: Set<string>;
28
+ }
29
+
30
+ /**
31
+ * Navigation actions
32
+ */
33
+ export type NavigationAction =
34
+ | { type: 'NAVIGATE'; location: NavigationLocation }
35
+ | { type: 'GO_BACK' }
36
+ | { type: 'SET_FOCUS'; focus: PanelFocus }
37
+ | { type: 'SWITCH_FOCUS' }
38
+ | { type: 'SELECT_INDEX'; panel: 'machines' | 'projects' | 'workspaces'; index: number }
39
+ | { type: 'MOVE_UP' }
40
+ | { type: 'MOVE_DOWN' }
41
+ | { type: 'TOGGLE_EXPANDED'; id: string }
42
+ | { type: 'RESET' };
43
+
44
+ /**
45
+ * Create initial navigation state
46
+ */
47
+ export function createInitialNavigationState(
48
+ startWithMachines: boolean = false
49
+ ): NavigationState {
50
+ return {
51
+ location: startWithMachines
52
+ ? { screen: 'machines' }
53
+ : { screen: 'projects', machineId: 'local' },
54
+ focus: startWithMachines ? 'machines' : 'projects',
55
+ history: [],
56
+ selectedIndices: {
57
+ machines: 0,
58
+ projects: 0,
59
+ workspaces: 0,
60
+ },
61
+ expanded: new Set(),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Navigation reducer
67
+ */
68
+ export function navigationReducer(
69
+ state: NavigationState,
70
+ action: NavigationAction,
71
+ counts: { machines: number; projects: number; workspaces: number }
72
+ ): NavigationState {
73
+ switch (action.type) {
74
+ case 'NAVIGATE': {
75
+ // Push current location to history before navigating
76
+ const newHistory = [...state.history, state.location];
77
+
78
+ // Determine focus based on screen
79
+ let focus: PanelFocus = state.focus;
80
+ if (action.location.screen === 'machines') {
81
+ focus = 'machines';
82
+ } else if (action.location.screen === 'projects') {
83
+ focus = 'projects';
84
+ } else if (action.location.screen === 'workspaces' || action.location.screen === 'session') {
85
+ focus = 'workspaces';
86
+ }
87
+
88
+ return {
89
+ ...state,
90
+ location: action.location,
91
+ history: newHistory,
92
+ focus,
93
+ };
94
+ }
95
+
96
+ case 'GO_BACK': {
97
+ if (state.history.length === 0) {
98
+ return state;
99
+ }
100
+ const newHistory = [...state.history];
101
+ const previousLocation = newHistory.pop()!;
102
+
103
+ return {
104
+ ...state,
105
+ location: previousLocation,
106
+ history: newHistory,
107
+ };
108
+ }
109
+
110
+ case 'SET_FOCUS': {
111
+ return {
112
+ ...state,
113
+ focus: action.focus,
114
+ };
115
+ }
116
+
117
+ case 'SWITCH_FOCUS': {
118
+ // Cycle through available panels based on current screen
119
+ const { screen } = state.location;
120
+ let nextFocus: PanelFocus;
121
+
122
+ if (screen === 'machines') {
123
+ // Only machines panel available
124
+ nextFocus = 'machines';
125
+ } else if (screen === 'projects') {
126
+ // Toggle between machines and projects
127
+ nextFocus = state.focus === 'machines' ? 'projects' : 'machines';
128
+ } else {
129
+ // Toggle between projects and workspaces
130
+ nextFocus = state.focus === 'projects' ? 'workspaces' : 'projects';
131
+ }
132
+
133
+ return {
134
+ ...state,
135
+ focus: nextFocus,
136
+ };
137
+ }
138
+
139
+ case 'SELECT_INDEX': {
140
+ const maxIndex = counts[action.panel] - 1;
141
+ const index = Math.max(0, Math.min(action.index, maxIndex));
142
+
143
+ return {
144
+ ...state,
145
+ selectedIndices: {
146
+ ...state.selectedIndices,
147
+ [action.panel]: index,
148
+ },
149
+ };
150
+ }
151
+
152
+ case 'MOVE_UP': {
153
+ const panel = state.focus === 'inbox' ? 'workspaces' : state.focus;
154
+ const currentIndex = state.selectedIndices[panel as keyof typeof state.selectedIndices] ?? 0;
155
+ const newIndex = Math.max(0, currentIndex - 1);
156
+
157
+ return {
158
+ ...state,
159
+ selectedIndices: {
160
+ ...state.selectedIndices,
161
+ [panel]: newIndex,
162
+ },
163
+ };
164
+ }
165
+
166
+ case 'MOVE_DOWN': {
167
+ const panel = state.focus === 'inbox' ? 'workspaces' : state.focus;
168
+ const maxIndex = counts[panel as keyof typeof counts] - 1;
169
+ const currentIndex = state.selectedIndices[panel as keyof typeof state.selectedIndices] ?? 0;
170
+ const newIndex = Math.min(maxIndex, currentIndex + 1);
171
+
172
+ return {
173
+ ...state,
174
+ selectedIndices: {
175
+ ...state.selectedIndices,
176
+ [panel]: newIndex,
177
+ },
178
+ };
179
+ }
180
+
181
+ case 'TOGGLE_EXPANDED': {
182
+ const newExpanded = new Set(state.expanded);
183
+ if (newExpanded.has(action.id)) {
184
+ newExpanded.delete(action.id);
185
+ } else {
186
+ newExpanded.add(action.id);
187
+ }
188
+ return {
189
+ ...state,
190
+ expanded: newExpanded,
191
+ };
192
+ }
193
+
194
+ case 'RESET': {
195
+ return createInitialNavigationState(state.location.screen === 'machines');
196
+ }
197
+
198
+ default:
199
+ return state;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Helper to get current machine ID from location
205
+ */
206
+ export function getCurrentMachineId(state: NavigationState): string | null {
207
+ const { location } = state;
208
+ if (location.screen === 'machines') return null;
209
+ return location.machineId;
210
+ }
211
+
212
+ /**
213
+ * Helper to get current project name from location
214
+ */
215
+ export function getCurrentProjectName(state: NavigationState): string | null {
216
+ const { location } = state;
217
+ if (location.screen === 'machines' || location.screen === 'projects') return null;
218
+ return location.projectName;
219
+ }
220
+
221
+ /**
222
+ * Helper to check if we can go back
223
+ */
224
+ export function canGoBack(state: NavigationState): boolean {
225
+ return state.history.length > 0;
226
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared module exports
3
+ *
4
+ * Platform-agnostic types, providers, hooks, and components for TUI and Web.
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ MachineStatus,
10
+ MachineInfo as SharedMachineInfo,
11
+ Project,
12
+ Workspace,
13
+ WorkspaceSession,
14
+ InboxItem,
15
+ InboxItemType,
16
+ SessionStream,
17
+ NavigationLocation,
18
+ PanelFocus,
19
+ } from './types.js';
20
+
21
+ // Providers
22
+ export type {
23
+ MachineProvider,
24
+ CreateSessionOptions,
25
+ AttachSessionOptions,
26
+ MachineProviderEvent,
27
+ MachineProviderEventHandler,
28
+ EventedMachineProvider,
29
+ RemoteMachineProviderConfig,
30
+ } from './providers/index.js';
31
+
32
+ export {
33
+ LocalMachineProvider,
34
+ getLocalMachineProvider,
35
+ RemoteMachineProvider,
36
+ createRemoteMachineProvider,
37
+ } from './providers/index.js';
38
+
39
+ // Hooks
40
+ export type {
41
+ NavigationState,
42
+ NavigationAction,
43
+ } from './hooks/index.js';
44
+
45
+ export {
46
+ createInitialNavigationState,
47
+ navigationReducer,
48
+ getCurrentMachineId,
49
+ getCurrentProjectName,
50
+ canGoBack,
51
+ } from './hooks/index.js';
52
+
53
+ // Components - MachineList
54
+ export type {
55
+ ConnectionStatus,
56
+ MachineInfo,
57
+ UseMachineListProps,
58
+ MachineListItem,
59
+ UseMachineListReturn,
60
+ } from './components/index.js';
61
+
62
+ export {
63
+ useMachineList,
64
+ formatLastSeen,
65
+ getStatusColor,
66
+ getMachineLabel,
67
+ } from './components/index.js';
68
+
69
+ // Components - SpacesBrowser
70
+ export type {
71
+ WorkspaceInfo,
72
+ SessionInfo,
73
+ TreeItem,
74
+ TreeItemWithState,
75
+ UseSpacesBrowserProps,
76
+ UseSpacesBrowserReturn,
77
+ } from './components/index.js';
78
+
79
+ export {
80
+ useSpacesBrowser,
81
+ formatTime,
82
+ } from './components/index.js';
83
+
84
+ // Components - Inbox
85
+ export type {
86
+ InboxItem as SharedInboxItem,
87
+ InboxItemType as SharedInboxItemType,
88
+ ParsedSessionName,
89
+ SessionGroup,
90
+ WorkspaceGroup,
91
+ ProjectGroup,
92
+ InboxDisplayItem,
93
+ UseInboxProps,
94
+ UseInboxReturn,
95
+ } from './components/index.js';
96
+
97
+ export {
98
+ useInbox,
99
+ parseSessionName,
100
+ getInboxIcon,
101
+ getInboxTypeLabel,
102
+ formatTimeAgo,
103
+ } from './components/index.js';
104
+
105
+ // Components - ProjectList
106
+ export type {
107
+ ProjectInfo,
108
+ ProjectListItem,
109
+ UseProjectListProps,
110
+ UseProjectListReturn,
111
+ } from './components/index.js';
112
+
113
+ export {
114
+ useProjectList,
115
+ getProjectDisplayName,
116
+ getShortRepoName,
117
+ formatWorkspaceCount,
118
+ } from './components/index.js';
119
+
120
+ // Components - renderers (import separately for tree-shaking)
121
+ // Web: import { MachineListWeb, SpacesBrowserWeb, InboxWeb, ProjectListWeb } from '@spaces/shared/components'
122
+ // TUI: import { MachineListTUI, SpacesBrowserTUI, InboxTUI, ProjectListTUI } from '@spaces/shared/components'