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.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
])
|
|
@@ -0,0 +1,16 @@
|
|
|
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>
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
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>
|
|
@@ -0,0 +1,604 @@
|
|
|
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
|
+
}
|