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,314 @@
1
+ /**
2
+ * TUI App State Management
3
+ *
4
+ * Unified state management for the TUI that works with both local and remote modes.
5
+ * Uses shared components and providers.
6
+ */
7
+
8
+ import { useState, useCallback, useEffect, useReducer } from 'react';
9
+ import type { MachineInfo } from '../../shared/components/index.js';
10
+ import type { MachineProvider } from '../../shared/providers/index.js';
11
+ import type { Project, Workspace, WorkspaceSession, InboxItem } from '../../shared/types.js';
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ /** App view modes */
18
+ export type AppView =
19
+ | 'machines' // Machine list (when in remote mode or has multiple machines)
20
+ | 'projects' // Project list (local mode default)
21
+ | 'workspaces' // Workspace browser for selected project
22
+ | 'terminal'; // Attached to session
23
+
24
+ /** Panel focus */
25
+ export type PanelFocus = 'projects' | 'workspaces';
26
+
27
+ /** App state */
28
+ export interface AppState {
29
+ // View state
30
+ view: AppView;
31
+ panelFocus: PanelFocus;
32
+
33
+ // Machine state (remote mode)
34
+ selectedMachine: MachineInfo | null;
35
+ machineProvider: MachineProvider | null;
36
+
37
+ // Project state
38
+ projects: Project[];
39
+ selectedProjectIndex: number;
40
+ currentProject: string | null;
41
+
42
+ // Workspace state
43
+ workspaces: Workspace[];
44
+ sessions: Map<string, WorkspaceSession[]>;
45
+ selectedWorkspaceIndex: number;
46
+ expandedWorkspaces: Set<string>;
47
+
48
+ // Inbox
49
+ inbox: InboxItem[];
50
+ unreadCount: number;
51
+
52
+ // UI state
53
+ isLoading: boolean;
54
+ error: string | null;
55
+ }
56
+
57
+ /** State actions */
58
+ export type AppAction =
59
+ | { type: 'SET_LOADING'; loading: boolean }
60
+ | { type: 'SET_ERROR'; error: string | null }
61
+ | { type: 'SET_VIEW'; view: AppView }
62
+ | { type: 'SET_PANEL_FOCUS'; focus: PanelFocus }
63
+ | { type: 'SET_MACHINE'; machine: MachineInfo | null; provider: MachineProvider | null }
64
+ | { type: 'SET_PROJECTS'; projects: Project[] }
65
+ | { type: 'SELECT_PROJECT'; index: number }
66
+ | { type: 'SET_CURRENT_PROJECT'; project: string | null }
67
+ | { type: 'SET_WORKSPACES'; workspaces: Workspace[] }
68
+ | { type: 'SET_SESSIONS'; workspaceId: string; sessions: WorkspaceSession[] }
69
+ | { type: 'SELECT_WORKSPACE'; index: number }
70
+ | { type: 'TOGGLE_WORKSPACE'; workspaceId: string }
71
+ | { type: 'SET_INBOX'; inbox: InboxItem[]; unreadCount: number }
72
+ | { type: 'SWITCH_PANEL' }
73
+ | { type: 'MOVE_UP' }
74
+ | { type: 'MOVE_DOWN' };
75
+
76
+ // ============================================================================
77
+ // Reducer
78
+ // ============================================================================
79
+
80
+ function appReducer(state: AppState, action: AppAction): AppState {
81
+ switch (action.type) {
82
+ case 'SET_LOADING':
83
+ return { ...state, isLoading: action.loading };
84
+
85
+ case 'SET_ERROR':
86
+ return { ...state, error: action.error };
87
+
88
+ case 'SET_VIEW':
89
+ return { ...state, view: action.view };
90
+
91
+ case 'SET_PANEL_FOCUS':
92
+ return { ...state, panelFocus: action.focus };
93
+
94
+ case 'SET_MACHINE':
95
+ return {
96
+ ...state,
97
+ selectedMachine: action.machine,
98
+ machineProvider: action.provider,
99
+ // Reset project/workspace state when switching machines
100
+ projects: [],
101
+ workspaces: [],
102
+ sessions: new Map(),
103
+ selectedProjectIndex: 0,
104
+ selectedWorkspaceIndex: 0,
105
+ expandedWorkspaces: new Set(),
106
+ };
107
+
108
+ case 'SET_PROJECTS':
109
+ return { ...state, projects: action.projects };
110
+
111
+ case 'SELECT_PROJECT': {
112
+ const index = Math.max(0, Math.min(action.index, state.projects.length - 1));
113
+ return { ...state, selectedProjectIndex: index };
114
+ }
115
+
116
+ case 'SET_CURRENT_PROJECT':
117
+ return { ...state, currentProject: action.project };
118
+
119
+ case 'SET_WORKSPACES':
120
+ return { ...state, workspaces: action.workspaces, selectedWorkspaceIndex: 0 };
121
+
122
+ case 'SET_SESSIONS': {
123
+ const newSessions = new Map(state.sessions);
124
+ newSessions.set(action.workspaceId, action.sessions);
125
+ return { ...state, sessions: newSessions };
126
+ }
127
+
128
+ case 'SELECT_WORKSPACE': {
129
+ const maxIndex = getMaxWorkspaceIndex(state);
130
+ const index = Math.max(0, Math.min(action.index, maxIndex));
131
+ return { ...state, selectedWorkspaceIndex: index };
132
+ }
133
+
134
+ case 'TOGGLE_WORKSPACE': {
135
+ const newExpanded = new Set(state.expandedWorkspaces);
136
+ if (newExpanded.has(action.workspaceId)) {
137
+ newExpanded.delete(action.workspaceId);
138
+ } else {
139
+ newExpanded.add(action.workspaceId);
140
+ }
141
+ return { ...state, expandedWorkspaces: newExpanded };
142
+ }
143
+
144
+ case 'SET_INBOX':
145
+ return { ...state, inbox: action.inbox, unreadCount: action.unreadCount };
146
+
147
+ case 'SWITCH_PANEL':
148
+ return {
149
+ ...state,
150
+ panelFocus: state.panelFocus === 'projects' ? 'workspaces' : 'projects',
151
+ };
152
+
153
+ case 'MOVE_UP':
154
+ if (state.panelFocus === 'projects') {
155
+ const index = Math.max(0, state.selectedProjectIndex - 1);
156
+ return { ...state, selectedProjectIndex: index };
157
+ } else {
158
+ const index = Math.max(0, state.selectedWorkspaceIndex - 1);
159
+ return { ...state, selectedWorkspaceIndex: index };
160
+ }
161
+
162
+ case 'MOVE_DOWN':
163
+ if (state.panelFocus === 'projects') {
164
+ const index = Math.min(state.projects.length - 1, state.selectedProjectIndex + 1);
165
+ return { ...state, selectedProjectIndex: index };
166
+ } else {
167
+ const maxIndex = getMaxWorkspaceIndex(state);
168
+ const index = Math.min(maxIndex, state.selectedWorkspaceIndex + 1);
169
+ return { ...state, selectedWorkspaceIndex: index };
170
+ }
171
+
172
+ default:
173
+ return state;
174
+ }
175
+ }
176
+
177
+ function getMaxWorkspaceIndex(state: AppState): number {
178
+ // Count total items in tree (workspaces + sessions + new-session options)
179
+ let count = 0;
180
+ for (const ws of state.workspaces) {
181
+ count++; // workspace itself
182
+ if (state.expandedWorkspaces.has(ws.name)) {
183
+ const sessions = state.sessions.get(ws.name) || [];
184
+ count += sessions.length; // sessions
185
+ count++; // new-session option
186
+ }
187
+ }
188
+ return Math.max(0, count - 1);
189
+ }
190
+
191
+ // ============================================================================
192
+ // Initial State
193
+ // ============================================================================
194
+
195
+ function createInitialState(isRemoteMode: boolean): AppState {
196
+ return {
197
+ view: isRemoteMode ? 'machines' : 'projects',
198
+ panelFocus: 'projects',
199
+ selectedMachine: null,
200
+ machineProvider: null,
201
+ projects: [],
202
+ selectedProjectIndex: 0,
203
+ currentProject: null,
204
+ workspaces: [],
205
+ sessions: new Map(),
206
+ selectedWorkspaceIndex: 0,
207
+ expandedWorkspaces: new Set(),
208
+ inbox: [],
209
+ unreadCount: 0,
210
+ isLoading: true,
211
+ error: null,
212
+ };
213
+ }
214
+
215
+ // ============================================================================
216
+ // Hook
217
+ // ============================================================================
218
+
219
+ export interface UseAppStateOptions {
220
+ isRemoteMode: boolean;
221
+ }
222
+
223
+ export function useAppState(options: UseAppStateOptions) {
224
+ const { isRemoteMode } = options;
225
+
226
+ const [state, dispatch] = useReducer(appReducer, isRemoteMode, createInitialState);
227
+
228
+ // Convenience methods
229
+ const setLoading = useCallback((loading: boolean) => {
230
+ dispatch({ type: 'SET_LOADING', loading });
231
+ }, []);
232
+
233
+ const setError = useCallback((error: string | null) => {
234
+ dispatch({ type: 'SET_ERROR', error });
235
+ }, []);
236
+
237
+ const setView = useCallback((view: AppView) => {
238
+ dispatch({ type: 'SET_VIEW', view });
239
+ }, []);
240
+
241
+ const setPanelFocus = useCallback((focus: PanelFocus) => {
242
+ dispatch({ type: 'SET_PANEL_FOCUS', focus });
243
+ }, []);
244
+
245
+ const setMachine = useCallback((machine: MachineInfo | null, provider: MachineProvider | null) => {
246
+ dispatch({ type: 'SET_MACHINE', machine, provider });
247
+ }, []);
248
+
249
+ const setProjects = useCallback((projects: Project[]) => {
250
+ dispatch({ type: 'SET_PROJECTS', projects });
251
+ }, []);
252
+
253
+ const selectProject = useCallback((index: number) => {
254
+ dispatch({ type: 'SELECT_PROJECT', index });
255
+ }, []);
256
+
257
+ const setCurrentProject = useCallback((project: string | null) => {
258
+ dispatch({ type: 'SET_CURRENT_PROJECT', project });
259
+ }, []);
260
+
261
+ const setWorkspaces = useCallback((workspaces: Workspace[]) => {
262
+ dispatch({ type: 'SET_WORKSPACES', workspaces });
263
+ }, []);
264
+
265
+ const setSessions = useCallback((workspaceId: string, sessions: WorkspaceSession[]) => {
266
+ dispatch({ type: 'SET_SESSIONS', workspaceId, sessions });
267
+ }, []);
268
+
269
+ const selectWorkspace = useCallback((index: number) => {
270
+ dispatch({ type: 'SELECT_WORKSPACE', index });
271
+ }, []);
272
+
273
+ const toggleWorkspace = useCallback((workspaceId: string) => {
274
+ dispatch({ type: 'TOGGLE_WORKSPACE', workspaceId });
275
+ }, []);
276
+
277
+ const setInbox = useCallback((inbox: InboxItem[], unreadCount: number) => {
278
+ dispatch({ type: 'SET_INBOX', inbox, unreadCount });
279
+ }, []);
280
+
281
+ const switchPanel = useCallback(() => {
282
+ dispatch({ type: 'SWITCH_PANEL' });
283
+ }, []);
284
+
285
+ const moveUp = useCallback(() => {
286
+ dispatch({ type: 'MOVE_UP' });
287
+ }, []);
288
+
289
+ const moveDown = useCallback(() => {
290
+ dispatch({ type: 'MOVE_DOWN' });
291
+ }, []);
292
+
293
+ return {
294
+ state,
295
+ dispatch,
296
+ // Actions
297
+ setLoading,
298
+ setError,
299
+ setView,
300
+ setPanelFocus,
301
+ setMachine,
302
+ setProjects,
303
+ selectProject,
304
+ setCurrentProject,
305
+ setWorkspaces,
306
+ setSessions,
307
+ selectWorkspace,
308
+ toggleWorkspace,
309
+ setInbox,
310
+ switchPanel,
311
+ moveUp,
312
+ moveDown,
313
+ };
314
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Hook for monitoring daemon status (tmux-lite and serve)
3
+ *
4
+ * Provides real-time status of:
5
+ * - tmux-lite server (sessions count, version)
6
+ * - serve daemon (relay connection, clients count)
7
+ */
8
+
9
+ import { useState, useEffect, useCallback } from 'react';
10
+ import { getStatus as getTmuxStatus, isServerRunning as isTmuxRunning } from '../../lib/tmux-lite/cli.js';
11
+ import { queryServeStatus, isServeRunning, type StatusResponse } from '../../serve/daemon.js';
12
+
13
+ /** Package version for comparison */
14
+ const PACKAGE_VERSION = '1.0.0';
15
+
16
+ /** Tmux daemon status */
17
+ export interface TmuxStatus {
18
+ running: boolean;
19
+ version?: string;
20
+ sessions?: number;
21
+ attached?: number;
22
+ uptime?: number;
23
+ }
24
+
25
+ /** Serve daemon status */
26
+ export interface ServeStatus {
27
+ running: boolean;
28
+ version?: string;
29
+ relayUrl?: string;
30
+ relayStatus?: 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
31
+ clients?: number;
32
+ uptime?: number;
33
+ hosting?: {
34
+ subdomain: string;
35
+ tunnelActive: boolean;
36
+ };
37
+ }
38
+
39
+ /** Combined daemon status */
40
+ export interface DaemonStatus {
41
+ tmux: TmuxStatus;
42
+ serve: ServeStatus;
43
+ versionMismatch: boolean;
44
+ packageVersion: string;
45
+ }
46
+
47
+ /** Hook options */
48
+ export interface UseDaemonStatusOptions {
49
+ /** Polling interval in ms (default: 5000) */
50
+ pollInterval?: number;
51
+ /** Whether to poll automatically (default: true) */
52
+ autoPoll?: boolean;
53
+ }
54
+
55
+ /** Hook return type */
56
+ export interface UseDaemonStatusReturn {
57
+ status: DaemonStatus;
58
+ refresh: () => Promise<void>;
59
+ isLoading: boolean;
60
+ }
61
+
62
+ /**
63
+ * Hook for monitoring daemon status
64
+ */
65
+ export function useDaemonStatus(options: UseDaemonStatusOptions = {}): UseDaemonStatusReturn {
66
+ const { pollInterval = 5000, autoPoll = true } = options;
67
+
68
+ const [status, setStatus] = useState<DaemonStatus>({
69
+ tmux: { running: false },
70
+ serve: { running: false },
71
+ versionMismatch: false,
72
+ packageVersion: PACKAGE_VERSION,
73
+ });
74
+ const [isLoading, setIsLoading] = useState(true);
75
+
76
+ const refresh = useCallback(async () => {
77
+ try {
78
+ // Query tmux status
79
+ let tmuxStatus: TmuxStatus = { running: false };
80
+ if (await isTmuxRunning()) {
81
+ try {
82
+ const tmux = await getTmuxStatus();
83
+ tmuxStatus = {
84
+ running: true,
85
+ version: tmux.version,
86
+ sessions: tmux.sessions,
87
+ attached: tmux.attached,
88
+ uptime: tmux.uptime,
89
+ };
90
+ } catch {
91
+ tmuxStatus = { running: true }; // Running but couldn't get details
92
+ }
93
+ }
94
+
95
+ // Query serve status
96
+ let serveStatus: ServeStatus = { running: false };
97
+ if (isServeRunning()) {
98
+ try {
99
+ const serve = await queryServeStatus();
100
+ if (serve) {
101
+ serveStatus = {
102
+ running: true,
103
+ version: serve.version,
104
+ relayUrl: serve.relay.url,
105
+ relayStatus: serve.relay.status,
106
+ clients: serve.clients,
107
+ uptime: serve.uptime,
108
+ hosting: serve.hosting,
109
+ };
110
+ } else {
111
+ serveStatus = { running: true }; // Running but couldn't get details
112
+ }
113
+ } catch {
114
+ serveStatus = { running: true }; // Running but couldn't get details
115
+ }
116
+ }
117
+
118
+ // Check version mismatch
119
+ const versionMismatch =
120
+ (tmuxStatus.version && tmuxStatus.version !== PACKAGE_VERSION) ||
121
+ (serveStatus.version && serveStatus.version !== PACKAGE_VERSION);
122
+
123
+ setStatus({
124
+ tmux: tmuxStatus,
125
+ serve: serveStatus,
126
+ versionMismatch: !!versionMismatch,
127
+ packageVersion: PACKAGE_VERSION,
128
+ });
129
+ } finally {
130
+ setIsLoading(false);
131
+ }
132
+ }, []);
133
+
134
+ // Initial load
135
+ useEffect(() => {
136
+ refresh();
137
+ }, [refresh]);
138
+
139
+ // Polling
140
+ useEffect(() => {
141
+ if (!autoPoll) return;
142
+
143
+ const interval = setInterval(refresh, pollInterval);
144
+ return () => clearInterval(interval);
145
+ }, [autoPoll, pollInterval, refresh]);
146
+
147
+ return { status, refresh, isLoading };
148
+ }
149
+
150
+ /**
151
+ * Format uptime in human-readable format
152
+ */
153
+ export function formatUptime(seconds: number): string {
154
+ if (seconds < 60) return `${seconds}s`;
155
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
156
+ const hours = Math.floor(seconds / 3600);
157
+ const mins = Math.floor((seconds % 3600) / 60);
158
+ if (hours < 24) return `${hours}h ${mins}m`;
159
+ const days = Math.floor(hours / 24);
160
+ return `${days}d ${hours % 24}h`;
161
+ }
162
+
163
+ /**
164
+ * Format relay status for display
165
+ */
166
+ export function formatRelayStatus(status: ServeStatus['relayStatus']): string {
167
+ switch (status) {
168
+ case 'connected': return '●';
169
+ case 'connecting': return '◐';
170
+ case 'reconnecting': return '◐';
171
+ case 'disconnected': return '○';
172
+ default: return '?';
173
+ }
174
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * TUI-specific wrapper for shared useInbox hook
3
+ *
4
+ * Integrates with TUI's renderer suspend/resume and tmux-lite CLI.
5
+ */
6
+
7
+ import { useCallback } from 'react';
8
+ import { spawn } from 'child_process';
9
+ import { useInbox, type UseInboxProps, type UseInboxReturn } from '../../shared/components/Inbox.js';
10
+ import type { InboxItem } from '../../lib/tmux-lite/cli.js';
11
+
12
+ interface UseTUIInboxOptions {
13
+ items: InboxItem[];
14
+ unreadCount: number;
15
+ onClose: () => void;
16
+ onRefreshWorkspaces: () => Promise<void>;
17
+ onRefreshInbox: () => Promise<{ items: InboxItem[]; unreadCount: number }>;
18
+ renderer: {
19
+ suspend: () => void;
20
+ resume: () => void;
21
+ };
22
+ }
23
+
24
+ interface UseTUIInboxReturn extends UseInboxReturn {
25
+ /** Refresh inbox after operations */
26
+ refreshInbox: () => Promise<void>;
27
+ }
28
+
29
+ export function useTUIInbox(options: UseTUIInboxOptions): UseTUIInboxReturn {
30
+ const {
31
+ items,
32
+ unreadCount,
33
+ onClose,
34
+ onRefreshWorkspaces,
35
+ onRefreshInbox,
36
+ renderer,
37
+ } = options;
38
+
39
+ // Clear a single inbox item
40
+ const onClearItem = useCallback(async (itemId: string) => {
41
+ const { clearInbox } = await import('../../lib/tmux-lite/cli.js');
42
+ await clearInbox(itemId);
43
+ }, []);
44
+
45
+ // Clear all inbox items
46
+ const onClearAll = useCallback(async () => {
47
+ const { clearInbox } = await import('../../lib/tmux-lite/cli.js');
48
+ await clearInbox();
49
+ }, []);
50
+
51
+ // Mark an item as read
52
+ const onMarkRead = useCallback(async (itemId: string) => {
53
+ const { markInboxRead } = await import('../../lib/tmux-lite/cli.js');
54
+ await markInboxRead(itemId);
55
+ }, []);
56
+
57
+ // Attach to a session
58
+ const onAttachSession = useCallback(async (sessionId: string) => {
59
+ const { listSessions } = await import('../../lib/tmux-lite/cli.js');
60
+ const sessions = await listSessions();
61
+ const session = sessions.find(s => s.id === sessionId);
62
+
63
+ if (!session) {
64
+ throw new Error('Session no longer exists');
65
+ }
66
+
67
+ // Get CLI path for tmux-lite
68
+ const cliPath = new URL('../../lib/tmux-lite/cli.ts', import.meta.url).pathname;
69
+
70
+ // Suspend TUI, attach to session, resume TUI
71
+ renderer.suspend();
72
+ const proc = spawn('bun', ['run', cliPath, 'attach', session.id, '-f'], { stdio: 'inherit' });
73
+ await new Promise<void>((resolve) => proc.on('exit', () => resolve()));
74
+ renderer.resume();
75
+
76
+ // Refresh workspaces after detaching
77
+ await onRefreshWorkspaces();
78
+ }, [renderer, onRefreshWorkspaces]);
79
+
80
+ // Convert TUI InboxItem to shared InboxItem format
81
+ const convertedItems = items.map(item => ({
82
+ id: item.id,
83
+ sessionId: item.sessionId,
84
+ sessionName: item.sessionName,
85
+ type: item.type as 'exit' | 'title' | 'idle' | 'bell',
86
+ context: item.context,
87
+ timestamp: item.timestamp,
88
+ read: item.read,
89
+ processTitle: item.processTitle,
90
+ exitCode: item.exitCode,
91
+ }));
92
+
93
+ // Use the shared hook
94
+ const hookReturn = useInbox({
95
+ items: convertedItems,
96
+ unreadCount,
97
+ onClearItem,
98
+ onClearAll,
99
+ onMarkRead,
100
+ onAttachSession,
101
+ onClose,
102
+ });
103
+
104
+ // Wrap refresh to also update the TUI state
105
+ const refreshInbox = useCallback(async () => {
106
+ await onRefreshInbox();
107
+ }, [onRefreshInbox]);
108
+
109
+ return {
110
+ ...hookReturn,
111
+ refreshInbox,
112
+ };
113
+ }