gitspace 0.2.0-rc.20 → 0.2.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -6
- package/.claude/settings.local.json +0 -10
- package/.gitspace/bundle.json +0 -50
- package/.gitspace/events.json +0 -11
- package/.gitspace/processes.json +0 -23
- package/.gitspace/scripts/select/01-status.sh +0 -39
- package/.gitspace/scripts/setup/01-install-deps.sh +0 -12
- package/.gitspace/scripts/setup/02-typecheck.sh +0 -16
- package/AGENTS.md +0 -469
- package/CLAUDE.md +0 -1
- package/bun.lock +0 -794
- package/docs/CONNECTION.md +0 -623
- package/docs/GATEWAY-WORKER.md +0 -319
- package/docs/GETTING-STARTED.md +0 -448
- package/docs/GITSPACE-PLATFORM.md +0 -1819
- package/docs/INFRASTRUCTURE.md +0 -1347
- package/docs/PROTOCOL.md +0 -619
- package/docs/QUICKSTART.md +0 -183
- package/docs/RELAY.md +0 -327
- package/docs/REMOTE-DESIGN.md +0 -554
- package/docs/ROADMAP.md +0 -564
- package/docs/SITE_DOCS_FIGMA_MAKE.md +0 -1176
- package/docs/STACK-DESIGN.md +0 -588
- package/docs/UNIFIED_ARCHITECTURE.md +0 -138
- package/experiments/pty-benchmark.ts +0 -148
- package/experiments/pty-latency.ts +0 -100
- package/experiments/router/client.ts +0 -199
- package/experiments/router/protocol.ts +0 -74
- package/experiments/router/router.ts +0 -217
- package/experiments/router/session.ts +0 -180
- package/experiments/router/test.ts +0 -133
- package/experiments/socket-bandwidth.ts +0 -77
- package/homebrew/gitspace.rb +0 -45
- package/landing-page/ATTRIBUTIONS.md +0 -3
- package/landing-page/README.md +0 -11
- package/landing-page/bun.lock +0 -801
- package/landing-page/guidelines/Guidelines.md +0 -61
- package/landing-page/index.html +0 -37
- package/landing-page/package.json +0 -90
- package/landing-page/postcss.config.mjs +0 -15
- package/landing-page/public/_redirects +0 -1
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +0 -53
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +0 -27
- package/landing-page/src/app/components/ui/accordion.tsx +0 -66
- package/landing-page/src/app/components/ui/alert-dialog.tsx +0 -157
- package/landing-page/src/app/components/ui/alert.tsx +0 -66
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +0 -11
- package/landing-page/src/app/components/ui/avatar.tsx +0 -53
- package/landing-page/src/app/components/ui/badge.tsx +0 -46
- package/landing-page/src/app/components/ui/breadcrumb.tsx +0 -109
- package/landing-page/src/app/components/ui/button.tsx +0 -57
- package/landing-page/src/app/components/ui/calendar.tsx +0 -75
- package/landing-page/src/app/components/ui/card.tsx +0 -92
- package/landing-page/src/app/components/ui/carousel.tsx +0 -241
- package/landing-page/src/app/components/ui/chart.tsx +0 -353
- package/landing-page/src/app/components/ui/checkbox.tsx +0 -32
- package/landing-page/src/app/components/ui/collapsible.tsx +0 -33
- package/landing-page/src/app/components/ui/command.tsx +0 -177
- package/landing-page/src/app/components/ui/context-menu.tsx +0 -252
- package/landing-page/src/app/components/ui/dialog.tsx +0 -135
- package/landing-page/src/app/components/ui/drawer.tsx +0 -132
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +0 -257
- package/landing-page/src/app/components/ui/form.tsx +0 -168
- package/landing-page/src/app/components/ui/hover-card.tsx +0 -44
- package/landing-page/src/app/components/ui/input-otp.tsx +0 -77
- package/landing-page/src/app/components/ui/input.tsx +0 -21
- package/landing-page/src/app/components/ui/label.tsx +0 -24
- package/landing-page/src/app/components/ui/menubar.tsx +0 -276
- package/landing-page/src/app/components/ui/navigation-menu.tsx +0 -168
- package/landing-page/src/app/components/ui/pagination.tsx +0 -127
- package/landing-page/src/app/components/ui/popover.tsx +0 -48
- package/landing-page/src/app/components/ui/progress.tsx +0 -31
- package/landing-page/src/app/components/ui/radio-group.tsx +0 -45
- package/landing-page/src/app/components/ui/resizable.tsx +0 -56
- package/landing-page/src/app/components/ui/scroll-area.tsx +0 -58
- package/landing-page/src/app/components/ui/select.tsx +0 -189
- package/landing-page/src/app/components/ui/separator.tsx +0 -28
- package/landing-page/src/app/components/ui/sheet.tsx +0 -139
- package/landing-page/src/app/components/ui/sidebar.tsx +0 -726
- package/landing-page/src/app/components/ui/skeleton.tsx +0 -13
- package/landing-page/src/app/components/ui/slider.tsx +0 -63
- package/landing-page/src/app/components/ui/sonner.tsx +0 -25
- package/landing-page/src/app/components/ui/switch.tsx +0 -31
- package/landing-page/src/app/components/ui/table.tsx +0 -116
- package/landing-page/src/app/components/ui/tabs.tsx +0 -66
- package/landing-page/src/app/components/ui/textarea.tsx +0 -18
- package/landing-page/src/app/components/ui/toggle-group.tsx +0 -73
- package/landing-page/src/app/components/ui/toggle.tsx +0 -47
- package/landing-page/src/app/components/ui/tooltip.tsx +0 -61
- package/landing-page/src/app/components/ui/use-mobile.ts +0 -21
- package/landing-page/src/app/components/ui/utils.ts +0 -6
- package/landing-page/src/components/docs/DocsContent.tsx +0 -801
- package/landing-page/src/components/docs/DocsSidebar.tsx +0 -90
- package/landing-page/src/components/landing/CTA.tsx +0 -59
- package/landing-page/src/components/landing/Comparison.tsx +0 -84
- package/landing-page/src/components/landing/FaultyTerminal.tsx +0 -424
- package/landing-page/src/components/landing/Features.tsx +0 -201
- package/landing-page/src/components/landing/Hero.tsx +0 -142
- package/landing-page/src/components/landing/Pricing.tsx +0 -140
- package/landing-page/src/components/landing/Roadmap.tsx +0 -86
- package/landing-page/src/components/landing/Security.tsx +0 -81
- package/landing-page/src/components/landing/TerminalWindow.tsx +0 -27
- package/landing-page/src/components/landing/UseCases.tsx +0 -55
- package/landing-page/src/components/landing/Workflow.tsx +0 -101
- package/landing-page/src/components/layout/DashboardNavbar.tsx +0 -37
- package/landing-page/src/components/layout/Footer.tsx +0 -55
- package/landing-page/src/components/layout/LandingNavbar.tsx +0 -82
- package/landing-page/src/components/ui/badge.tsx +0 -39
- package/landing-page/src/components/ui/breadcrumb.tsx +0 -115
- package/landing-page/src/components/ui/button.tsx +0 -57
- package/landing-page/src/components/ui/card.tsx +0 -79
- package/landing-page/src/components/ui/mock-terminal.tsx +0 -68
- package/landing-page/src/components/ui/separator.tsx +0 -28
- package/landing-page/src/lib/utils.ts +0 -6
- package/landing-page/src/main.tsx +0 -10
- package/landing-page/src/pages/Dashboard.tsx +0 -133
- package/landing-page/src/pages/DocsPage.tsx +0 -79
- package/landing-page/src/pages/LandingPage.tsx +0 -31
- package/landing-page/src/pages/TerminalView.tsx +0 -106
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +0 -3
- package/landing-page/src/styles/tailwind.css +0 -4
- package/landing-page/src/styles/theme.css +0 -181
- package/landing-page/vite.config.ts +0 -19
- package/scripts/GHOSTTY_TAB_BUG.md +0 -106
- package/scripts/build.ts +0 -298
- package/scripts/migrate-secrets.ts +0 -77
- package/scripts/release.ts +0 -140
- package/scripts/sample-events.ts +0 -263
- package/scripts/test-tabs-minimal.ts +0 -68
- package/scripts/test-tabs-workaround.ts +0 -95
- package/scripts/test-tabs.ts +0 -171
- package/src/__tests__/test-utils.ts +0 -298
- package/src/app/input/__tests__/sessionCommands.test.ts +0 -40
- package/src/app/input/sessionCommands.ts +0 -94
- package/src/app/session/__tests__/useAttachController.test.ts +0 -229
- package/src/app/session/createSessionBackend.bun.ts +0 -76
- package/src/app/session/createSessionBackend.web.ts +0 -104
- package/src/app/session/types.ts +0 -16
- package/src/app/session/useAttachController.ts +0 -220
- package/src/app/session/useProcessActions.ts +0 -201
- package/src/app/session/useSessionClient.ts +0 -35
- package/src/app/session/useWorkspaceDeleteFlow.ts +0 -170
- package/src/app.tui.tsx +0 -2929
- package/src/app.web.tsx +0 -1454
- package/src/commands/__tests__/connect-key.test.ts +0 -10
- package/src/commands/__tests__/events.test.ts +0 -201
- package/src/commands/__tests__/notifications.test.ts +0 -349
- package/src/commands/__tests__/process.test.ts +0 -251
- package/src/commands/__tests__/serve-messages.test.ts +0 -190
- package/src/commands/__tests__/serve-process-hosting.test.ts +0 -63
- package/src/commands/access.ts +0 -298
- package/src/commands/add.ts +0 -455
- package/src/commands/auth.ts +0 -369
- package/src/commands/bundle.ts +0 -232
- package/src/commands/config.ts +0 -242
- package/src/commands/connect-key.ts +0 -1
- package/src/commands/connect.ts +0 -576
- package/src/commands/directory.ts +0 -16
- package/src/commands/events.ts +0 -157
- package/src/commands/host.ts +0 -566
- package/src/commands/identity.ts +0 -184
- package/src/commands/linear.ts +0 -717
- package/src/commands/list.ts +0 -181
- package/src/commands/migrate.ts +0 -52
- package/src/commands/notifications.ts +0 -351
- package/src/commands/process.ts +0 -104
- package/src/commands/relay.ts +0 -315
- package/src/commands/remove.ts +0 -279
- package/src/commands/review.ts +0 -787
- package/src/commands/serve.ts +0 -1946
- package/src/commands/share.ts +0 -451
- package/src/commands/status.ts +0 -125
- package/src/commands/switch.ts +0 -361
- package/src/commands/tmux.ts +0 -317
- package/src/components/DPad.web.tsx +0 -343
- package/src/components/DiffViewer.web.tsx +0 -1192
- package/src/components/Events.tsx +0 -137
- package/src/components/Events.tui.tsx +0 -129
- package/src/components/Events.web.tsx +0 -386
- package/src/components/FloatingControls.web.tsx +0 -112
- package/src/components/FloatingJogWheel.web.tsx +0 -240
- package/src/components/Flow.tsx +0 -458
- package/src/components/Flow.tui.tsx +0 -343
- package/src/components/Flow.web.tsx +0 -442
- package/src/components/Inbox.tsx +0 -448
- package/src/components/Inbox.tui.tsx +0 -262
- package/src/components/Inbox.web.tsx +0 -329
- package/src/components/MachineList.tsx +0 -187
- package/src/components/MachineList.tui.tsx +0 -161
- package/src/components/MachineList.web.tsx +0 -210
- package/src/components/NumPad.web.tsx +0 -270
- package/src/components/ProjectList.tsx +0 -175
- package/src/components/ProjectList.tui.tsx +0 -109
- package/src/components/ProjectList.web.tsx +0 -143
- package/src/components/ProjectOnboardingStep.ts +0 -23
- package/src/components/ProjectOnboardingStep.tui.tsx +0 -88
- package/src/components/ProjectOnboardingStep.web.tsx +0 -59
- package/src/components/RemoteMachineScreen.tui.tsx +0 -690
- package/src/components/ScriptTerminal.tui.tsx +0 -160
- package/src/components/ScriptTerminal.web.tsx +0 -89
- package/src/components/SessionTerminal.tui.tsx +0 -406
- package/src/components/SessionTerminal.web.tsx +0 -467
- package/src/components/SpacesBrowser.tsx +0 -540
- package/src/components/SpacesBrowser.tui.tsx +0 -258
- package/src/components/SpacesBrowser.web.tsx +0 -332
- package/src/components/TerminalControls.web.tsx +0 -464
- package/src/components/ThreadPanel.web.tsx +0 -798
- package/src/components/__tests__/SpacesBrowser.test.ts +0 -541
- package/src/components/__tests__/SpacesBrowser.tui.test.tsx +0 -249
- package/src/components/__tests__/script-terminal-buffer.tui.test.ts +0 -72
- package/src/components/index.ts +0 -105
- package/src/components/review-decision-colors.ts +0 -11
- package/src/components/script-terminal-buffer.tui.ts +0 -37
- package/src/components/session-terminal-page-navigation.ts +0 -48
- package/src/components/terminal-bracketed-paste.tui.test.ts +0 -43
- package/src/components/terminal-bracketed-paste.tui.ts +0 -46
- package/src/core/__tests__/access.test.ts +0 -240
- package/src/core/__tests__/bundle-refresh.test.ts +0 -567
- package/src/core/__tests__/bundle.test.ts +0 -209
- package/src/core/__tests__/github-review.test.ts +0 -781
- package/src/core/__tests__/project-lifecycle.test.ts +0 -137
- package/src/core/__tests__/workspace-lifecycle.test.ts +0 -159
- package/src/core/__tests__/workspace.test.ts +0 -149
- package/src/core/access.ts +0 -277
- package/src/core/bundle-refresh.ts +0 -1064
- package/src/core/bundle.ts +0 -326
- package/src/core/config.ts +0 -405
- package/src/core/git.ts +0 -768
- package/src/core/github-review.ts +0 -761
- package/src/core/github.ts +0 -151
- package/src/core/identity.ts +0 -631
- package/src/core/linear.ts +0 -403
- package/src/core/preferences-service.ts +0 -17
- package/src/core/project-catalog.ts +0 -52
- package/src/core/project-lifecycle.ts +0 -163
- package/src/core/review-executor.ts +0 -316
- package/src/core/review.ts +0 -407
- package/src/core/secret-runtime.ts +0 -167
- package/src/core/shell.ts +0 -117
- package/src/core/trusted-relays.ts +0 -315
- package/src/core/workspace-lifecycle.ts +0 -216
- package/src/core/workspace.ts +0 -363
- package/src/hooks/__tests__/useLocalSession.tui.test.ts +0 -557
- package/src/hooks/index.ts +0 -8
- package/src/hooks/index.tui.ts +0 -32
- package/src/hooks/useDaemonStatus.tui.ts +0 -174
- package/src/hooks/useLocalSession.tui.ts +0 -395
- package/src/hooks/useRelayConnection.web.ts +0 -54
- package/src/hooks/useRemoteMachines.tui.ts +0 -166
- package/src/hooks/useRemoteTerminal.tui.ts +0 -22
- package/src/hooks/useReview.web.ts +0 -248
- package/src/hooks/useTerminal.web.ts +0 -36
- package/src/hooks/useUserActivity.ts +0 -61
- package/src/hooks/useVisualViewport.web.ts +0 -104
- package/src/index.ts +0 -1376
- package/src/lib/events/__tests__/collector-filter.test.ts +0 -105
- package/src/lib/events/__tests__/store-query.test.ts +0 -103
- package/src/lib/events/collector.ts +0 -494
- package/src/lib/events/filters.ts +0 -26
- package/src/lib/events/index.ts +0 -11
- package/src/lib/events/indexer.ts +0 -14
- package/src/lib/events/paths.ts +0 -69
- package/src/lib/events/reader.ts +0 -212
- package/src/lib/events/store.ts +0 -141
- package/src/lib/invite.web.ts +0 -58
- package/src/lib/preferences-service.web.ts +0 -41
- package/src/lib/processes/__tests__/config.test.ts +0 -83
- package/src/lib/processes/__tests__/names.test.ts +0 -125
- package/src/lib/processes/__tests__/schema.test.ts +0 -208
- package/src/lib/processes/__tests__/watchdog.test.ts +0 -210
- package/src/lib/processes/autostart.ts +0 -16
- package/src/lib/processes/config.ts +0 -187
- package/src/lib/processes/control.ts +0 -53
- package/src/lib/processes/editor.ts +0 -32
- package/src/lib/processes/events-config.ts +0 -37
- package/src/lib/processes/index.ts +0 -14
- package/src/lib/processes/instances.ts +0 -20
- package/src/lib/processes/manager.ts +0 -131
- package/src/lib/processes/names.ts +0 -71
- package/src/lib/processes/registry.ts +0 -26
- package/src/lib/processes/runner.ts +0 -211
- package/src/lib/processes/scheduler.ts +0 -17
- package/src/lib/processes/schema.ts +0 -74
- package/src/lib/processes/session-list.ts +0 -15
- package/src/lib/processes/state.ts +0 -82
- package/src/lib/processes/watchdog.test.ts +0 -79
- package/src/lib/processes/watchdog.ts +0 -106
- package/src/lib/remote-session/__tests__/protocol.test.ts +0 -291
- package/src/lib/remote-session/index.ts +0 -7
- package/src/lib/remote-session/protocol.ts +0 -443
- package/src/lib/remote-session/session-handler.ts +0 -1298
- package/src/lib/remote-session/workspace-scanner.ts +0 -161
- package/src/lib/sonner.web.ts +0 -1
- package/src/lib/storage/identity-store.web.ts +0 -94
- package/src/lib/tmux-lite/README.md +0 -81
- package/src/lib/tmux-lite/cli.ts +0 -855
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +0 -349
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +0 -291
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +0 -142
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +0 -339
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +0 -477
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +0 -499
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +0 -371
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +0 -573
- package/src/lib/tmux-lite/crypto/access-control.test.ts +0 -512
- package/src/lib/tmux-lite/crypto/access-control.ts +0 -320
- package/src/lib/tmux-lite/crypto/frames.test.ts +0 -262
- package/src/lib/tmux-lite/crypto/frames.ts +0 -141
- package/src/lib/tmux-lite/crypto/handshake.ts +0 -894
- package/src/lib/tmux-lite/crypto/identity.test.ts +0 -220
- package/src/lib/tmux-lite/crypto/identity.ts +0 -286
- package/src/lib/tmux-lite/crypto/index.ts +0 -51
- package/src/lib/tmux-lite/crypto/invites.test.ts +0 -381
- package/src/lib/tmux-lite/crypto/invites.ts +0 -215
- package/src/lib/tmux-lite/crypto/keyexchange.ts +0 -435
- package/src/lib/tmux-lite/crypto/keys.test.ts +0 -58
- package/src/lib/tmux-lite/crypto/keys.ts +0 -47
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +0 -169
- package/src/lib/tmux-lite/crypto/secretbox.ts +0 -124
- package/src/lib/tmux-lite/handshake-handler.ts +0 -451
- package/src/lib/tmux-lite/process-run.integration.test.ts +0 -266
- package/src/lib/tmux-lite/protocol.test.ts +0 -307
- package/src/lib/tmux-lite/protocol.ts +0 -291
- package/src/lib/tmux-lite/relay-client.ts +0 -506
- package/src/lib/tmux-lite/server-lifecycle.test.ts +0 -212
- package/src/lib/tmux-lite/server.ts +0 -1412
- package/src/lib/tmux-lite/shell-integration.sh +0 -37
- package/src/lib/tmux-lite/terminal-queries.test.ts +0 -54
- package/src/lib/tmux-lite/terminal-queries.ts +0 -49
- package/src/notifications/__tests__/useNotifications.test.ts +0 -739
- package/src/notifications/index.ts +0 -32
- package/src/notifications/policy.test.ts +0 -424
- package/src/notifications/policy.ts +0 -139
- package/src/notifications/types.ts +0 -82
- package/src/notifications/useNotifications.ts +0 -350
- package/src/pages/ReviewPage.web.tsx +0 -511
- package/src/preferences/index.ts +0 -1
- package/src/preferences/types.ts +0 -9
- package/src/relay/__tests__/e2e-flow.test.ts +0 -1284
- package/src/relay/__tests__/helpers/auth.ts +0 -354
- package/src/relay/__tests__/helpers/ports.ts +0 -51
- package/src/relay/__tests__/protocol-validation.test.ts +0 -265
- package/src/relay/authorization.ts +0 -303
- package/src/relay/embedded-assets.generated.d.ts +0 -15
- package/src/relay/identity.ts +0 -352
- package/src/relay/index.ts +0 -57
- package/src/relay/pipes.test.ts +0 -427
- package/src/relay/pipes.ts +0 -195
- package/src/relay/protocol.ts +0 -804
- package/src/relay/registries.test.ts +0 -437
- package/src/relay/registries.ts +0 -593
- package/src/relay/server.test.ts +0 -1323
- package/src/relay/server.ts +0 -1128
- package/src/relay/signing.ts +0 -238
- package/src/relay/types.ts +0 -69
- package/src/relay-client/__tests__/machine-directory-client.test.ts +0 -152
- package/src/relay-client/__tests__/useMachineDirectory.test.ts +0 -172
- package/src/relay-client/adapters/browser.ts +0 -27
- package/src/relay-client/adapters/node.ts +0 -29
- package/src/relay-client/index.ts +0 -33
- package/src/relay-client/machine-directory-client.ts +0 -244
- package/src/relay-client/useMachineDirectory.ts +0 -175
- package/src/serve/client-session-manager.ts +0 -635
- package/src/serve/daemon.ts +0 -497
- package/src/serve/pty-session.ts +0 -236
- package/src/serve/types.ts +0 -174
- package/src/session/__tests__/backend-manager.test.ts +0 -101
- package/src/session/__tests__/local-session-backend.test.ts +0 -1129
- package/src/session/__tests__/reducer.test.ts +0 -80
- package/src/session/__tests__/remote-session-backend.test.ts +0 -995
- package/src/session/__tests__/session-name.test.ts +0 -35
- package/src/session/__tests__/useBundleRefreshAttachFlow.test.ts +0 -431
- package/src/session/__tests__/useRemoteSessionClient.test.ts +0 -424
- package/src/session/__tests__/workspace-shell-hooks.integration.test.ts +0 -268
- package/src/session/__tests__/workspace-shell-hooks.test.ts +0 -24
- package/src/session/adapters/browser-remote.ts +0 -101
- package/src/session/adapters/node-remote.ts +0 -135
- package/src/session/backend-key.ts +0 -5
- package/src/session/backend-manager.ts +0 -80
- package/src/session/backend.ts +0 -93
- package/src/session/backends/local-session-backend.ts +0 -1119
- package/src/session/backends/remote-session-backend.ts +0 -1378
- package/src/session/crypto/__tests__/web-terminal.test.ts +0 -1158
- package/src/session/crypto/frames.web.ts +0 -205
- package/src/session/crypto/handshake.web.ts +0 -396
- package/src/session/crypto/identity.web.ts +0 -133
- package/src/session/crypto/keyexchange.web.ts +0 -246
- package/src/session/crypto/relay-signing.web.ts +0 -53
- package/src/session/events.ts +0 -38
- package/src/session/index.ts +0 -116
- package/src/session/reducer.ts +0 -274
- package/src/session/selectors.ts +0 -28
- package/src/session/session-name.ts +0 -50
- package/src/session/types.ts +0 -101
- package/src/session/useBundleRefreshAttachFlow.ts +0 -608
- package/src/session/useRemoteSessionClient.ts +0 -424
- package/src/session/useSessionEngine.ts +0 -432
- package/src/session/workspace-shell-hooks.ts +0 -35
- package/src/tui/__tests__/input-text.test.ts +0 -24
- package/src/tui/__tests__/local-terminal-sync.test.ts +0 -82
- package/src/tui/__tests__/session-terminal-page-navigation.test.ts +0 -94
- package/src/tui/app.tsx +0 -2
- package/src/tui/index.ts +0 -18
- package/src/tui/input-text.ts +0 -38
- package/src/tui/local-terminal-sync.ts +0 -41
- package/src/types/bundle-refresh.ts +0 -42
- package/src/types/bundle.ts +0 -130
- package/src/types/config.ts +0 -287
- package/src/types/errors.ts +0 -292
- package/src/types/events.ts +0 -91
- package/src/types/identity.ts +0 -284
- package/src/types/processes.ts +0 -45
- package/src/types/review.ts +0 -349
- package/src/types/script-phase.ts +0 -3
- package/src/types/workspace-fuzzy.ts +0 -49
- package/src/types/workspace.ts +0 -151
- package/src/utils/__tests__/onboarding.test.ts +0 -358
- package/src/utils/__tests__/run-scripts.test.ts +0 -535
- package/src/utils/__tests__/run-workspace-scripts.test.ts +0 -406
- package/src/utils/__tests__/workspace-setup.integration.test.ts +0 -633
- package/src/utils/__tests__/workspace-state.test.ts +0 -78
- package/src/utils/bun-socket-writer.ts +0 -80
- package/src/utils/clipboard.ts +0 -53
- package/src/utils/deps.test.ts +0 -31
- package/src/utils/deps.ts +0 -145
- package/src/utils/device.web.ts +0 -163
- package/src/utils/fuzzy-match.ts +0 -125
- package/src/utils/hostnames.ts +0 -43
- package/src/utils/hunk-header.ts +0 -17
- package/src/utils/id.ts +0 -9
- package/src/utils/logger.ts +0 -127
- package/src/utils/markdown.ts +0 -254
- package/src/utils/normalize-env-key.ts +0 -13
- package/src/utils/onboarding.ts +0 -279
- package/src/utils/prompts.ts +0 -176
- package/src/utils/run-commands.ts +0 -112
- package/src/utils/run-scripts.ts +0 -337
- package/src/utils/run-workspace-scripts.ts +0 -355
- package/src/utils/sanitize.test.ts +0 -149
- package/src/utils/sanitize.ts +0 -162
- package/src/utils/secrets.ts +0 -836
- package/src/utils/shell-escape.ts +0 -40
- package/src/utils/utf8.ts +0 -79
- package/src/utils/workspace-id.ts +0 -55
- package/src/utils/workspace-state.ts +0 -427
- package/src/version.generated.d.ts +0 -2
- package/todo-security.md +0 -92
- package/tsconfig.json +0 -29
- package/web/README.md +0 -73
- package/web/bun.lock +0 -675
- package/web/eslint.config.js +0 -23
- package/web/index.css +0 -249
- package/web/index.html +0 -16
- package/web/main.tsx +0 -10
- package/web/package.json +0 -39
- package/web/public/vite.svg +0 -1
- package/web/tsconfig.app.json +0 -35
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -39
- package/worker/bun.lock +0 -237
- package/worker/package.json +0 -22
- package/worker/schema.sql +0 -96
- package/worker/src/handlers/auth.ts +0 -451
- package/worker/src/handlers/subdomains.ts +0 -376
- package/worker/src/handlers/user.ts +0 -98
- package/worker/src/index.ts +0 -70
- package/worker/src/middleware/auth.ts +0 -152
- package/worker/src/services/cloudflare.ts +0 -609
- package/worker/src/types.ts +0 -96
- package/worker/tsconfig.json +0 -15
- package/worker/wrangler.toml +0 -26
package/src/app.web.tsx
DELETED
|
@@ -1,1454 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
3
|
-
import { SessionTerminal, type SessionTerminalHandle } from "./components/SessionTerminal.web";
|
|
4
|
-
import { ScriptTerminal } from "./components/ScriptTerminal.web";
|
|
5
|
-
import {
|
|
6
|
-
TerminalControls,
|
|
7
|
-
applyModifiersToInput,
|
|
8
|
-
type ModifierState,
|
|
9
|
-
} from "./components/TerminalControls.web";
|
|
10
|
-
import { FloatingControls } from "./components/FloatingControls.web";
|
|
11
|
-
import { useTerminal } from "./hooks/useTerminal.web";
|
|
12
|
-
import { useRelayConnection } from "./hooks/useRelayConnection.web";
|
|
13
|
-
import { useVisualViewport } from "./hooks/useVisualViewport.web";
|
|
14
|
-
import { parseInviteFromHash } from "./lib/invite.web";
|
|
15
|
-
import { browserPreferencesService } from "./lib/preferences-service.web";
|
|
16
|
-
import { Toaster, toast } from "./lib/sonner.web";
|
|
17
|
-
import { applyDeviceClasses, isMobileLayout, isTouchDevice } from "./utils/device.web";
|
|
18
|
-
import { useUserActivity } from "./hooks/index.js";
|
|
19
|
-
import { useBundleRefreshAttachFlow } from './session/useBundleRefreshAttachFlow.js';
|
|
20
|
-
import { useAttachController } from './app/session/useAttachController.js';
|
|
21
|
-
import { useProcessActions } from './app/session/useProcessActions.js';
|
|
22
|
-
import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
|
|
23
|
-
import { ReviewPage } from './pages/ReviewPage.web.js';
|
|
24
|
-
import { buildEditProcessesCommand } from './lib/processes/editor.js';
|
|
25
|
-
|
|
26
|
-
// Import shared components and hooks
|
|
27
|
-
import {
|
|
28
|
-
useMachineList,
|
|
29
|
-
useSpacesBrowser,
|
|
30
|
-
useFlow,
|
|
31
|
-
getDefaultShortcuts,
|
|
32
|
-
type MachineInfo,
|
|
33
|
-
type WorkspaceInfo,
|
|
34
|
-
} from "./components/index.js";
|
|
35
|
-
import { MachineListWeb } from "./components/MachineList.web.js";
|
|
36
|
-
import { SpacesBrowserWeb } from "./components/SpacesBrowser.web.js";
|
|
37
|
-
import { FlowWeb } from "./components/Flow.web.js";
|
|
38
|
-
import { useInbox } from "./components/Inbox.js";
|
|
39
|
-
import { InboxWeb } from "./components/Inbox.web.js";
|
|
40
|
-
import { useEvents, toWideEventItem, type WideEventItem } from "./components/Events.js";
|
|
41
|
-
import { EventsWeb } from "./components/Events.web.js";
|
|
42
|
-
import type { WideEventFilter } from "./types/events.js";
|
|
43
|
-
import {
|
|
44
|
-
useNotifications,
|
|
45
|
-
type ToastNotification,
|
|
46
|
-
type NotificationConfig,
|
|
47
|
-
DEFAULT_NOTIFICATION_CONFIG,
|
|
48
|
-
getSessionLabel,
|
|
49
|
-
} from "./notifications/index.js";
|
|
50
|
-
import {
|
|
51
|
-
resolveInboxCommand,
|
|
52
|
-
resolveMachineListCommand,
|
|
53
|
-
resolveSessionBrowserCommand,
|
|
54
|
-
} from './app/input/sessionCommands.js';
|
|
55
|
-
|
|
56
|
-
type View = "machines" | "terminal" | "review";
|
|
57
|
-
|
|
58
|
-
const PAGE_UP = '\x1b[5~';
|
|
59
|
-
const PAGE_DOWN = '\x1b[6~';
|
|
60
|
-
const DELETE_ERROR_CODES = new Set([
|
|
61
|
-
'REMOVE_SCRIPT_FAILED',
|
|
62
|
-
'DELETE_FAILED',
|
|
63
|
-
'WORKSPACE_NOT_FOUND',
|
|
64
|
-
'RESOURCE_NOT_FOUND',
|
|
65
|
-
'NOT_FOUND',
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
export default function App() {
|
|
69
|
-
const [view, setView] = useState<View>("machines");
|
|
70
|
-
const [selectedMachine, setSelectedMachine] = useState<MachineInfo | null>(null);
|
|
71
|
-
const [showInbox, setShowInbox] = useState(false);
|
|
72
|
-
const [showScriptTerminal, setShowScriptTerminal] = useState(false);
|
|
73
|
-
const [scriptWorkspaceName, setScriptWorkspaceName] = useState('workspace');
|
|
74
|
-
const [copied, setCopied] = useState(false);
|
|
75
|
-
const [showMobileControls, setShowMobileControls] = useState(false);
|
|
76
|
-
const [inputMode, setInputMode] = useState(false);
|
|
77
|
-
const [showEvents, setShowEvents] = useState(false);
|
|
78
|
-
const [eventsWorkspacePath, setEventsWorkspacePath] = useState<string | null>(null);
|
|
79
|
-
const [eventsWorkspaceLabel, setEventsWorkspaceLabel] = useState<string>('');
|
|
80
|
-
const [pendingProcessEditWorkspaceId, setPendingProcessEditWorkspaceId] = useState<string | null>(null);
|
|
81
|
-
const [modifiers, setModifiers] = useState<ModifierState>({
|
|
82
|
-
ctrl: false,
|
|
83
|
-
shift: false,
|
|
84
|
-
alt: false,
|
|
85
|
-
});
|
|
86
|
-
const [localNotificationConfig, setLocalNotificationConfig] =
|
|
87
|
-
useState<NotificationConfig | null>(null);
|
|
88
|
-
const [isViewOnlySession, setIsViewOnlySession] = useState(false);
|
|
89
|
-
const pendingProcessEditWorkspacesRef = useRef<unknown[] | null>(null);
|
|
90
|
-
const pendingProcessEditValidationArmedRef = useRef(false);
|
|
91
|
-
const eventsKeyboardStateRef = useRef<{
|
|
92
|
-
selectedIndex: number;
|
|
93
|
-
selectIndex: (index: number) => void;
|
|
94
|
-
} | null>(null);
|
|
95
|
-
|
|
96
|
-
// Terminal ref for external control (focus, sendData)
|
|
97
|
-
const terminalRef = useRef<SessionTerminalHandle>(null);
|
|
98
|
-
const lastScriptErrorRef = useRef<string | null>(null);
|
|
99
|
-
const lastCommandErrorRef = useRef<string | null>(null);
|
|
100
|
-
const suppressDeleteScriptFailureModalRef = useRef(false);
|
|
101
|
-
|
|
102
|
-
// Invite params from URL
|
|
103
|
-
const [inviteParams, setInviteParams] = useState<{
|
|
104
|
-
machineId?: string;
|
|
105
|
-
inviteId?: string;
|
|
106
|
-
inviteToken?: string;
|
|
107
|
-
} | null>(null);
|
|
108
|
-
|
|
109
|
-
// Review workspace/project state
|
|
110
|
-
const [reviewWorkspace, setReviewWorkspace] = useState<{
|
|
111
|
-
projectName: string;
|
|
112
|
-
workspaceId: string;
|
|
113
|
-
workspaceLabel?: string;
|
|
114
|
-
} | null>(null);
|
|
115
|
-
|
|
116
|
-
// Relay connection (for machine list)
|
|
117
|
-
const relay = useRelayConnection();
|
|
118
|
-
|
|
119
|
-
// Terminal connection (for PTY)
|
|
120
|
-
const terminal = useTerminal();
|
|
121
|
-
const activeNotificationConfig =
|
|
122
|
-
terminal.notificationConfig ?? localNotificationConfig ?? DEFAULT_NOTIFICATION_CONFIG;
|
|
123
|
-
|
|
124
|
-
// Visual viewport hook for keyboard detection
|
|
125
|
-
const keyboardVisible = useVisualViewport();
|
|
126
|
-
|
|
127
|
-
// Apply device-specific CSS classes and detect mobile on mount
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
applyDeviceClasses();
|
|
130
|
-
// Show mobile controls on touch devices or mobile layout
|
|
131
|
-
setShowMobileControls(isTouchDevice() || isMobileLayout());
|
|
132
|
-
|
|
133
|
-
// Listen for layout changes
|
|
134
|
-
const mediaQuery = window.matchMedia('(max-width: 767px)');
|
|
135
|
-
const handleChange = (e: MediaQueryListEvent) => {
|
|
136
|
-
setShowMobileControls(e.matches || isTouchDevice());
|
|
137
|
-
};
|
|
138
|
-
mediaQuery.addEventListener('change', handleChange);
|
|
139
|
-
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
140
|
-
}, []);
|
|
141
|
-
|
|
142
|
-
// Flow/Modal system
|
|
143
|
-
const flow = useFlow({
|
|
144
|
-
onError: (error) => console.error('Flow error:', error),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const bundleRefreshAttach = useBundleRefreshAttachFlow({
|
|
148
|
-
flow,
|
|
149
|
-
commandError: terminal.commandError,
|
|
150
|
-
attachSession: (params) => terminal.attachSession(params),
|
|
151
|
-
getBundleRefreshPlan: terminal.getBundleRefreshPlan,
|
|
152
|
-
applyBundleRefresh: terminal.applyBundleRefresh,
|
|
153
|
-
resolveProjectName: (workspaceId) => {
|
|
154
|
-
const index = workspaceId.indexOf(':');
|
|
155
|
-
if (index > 0) {
|
|
156
|
-
return workspaceId.slice(0, index);
|
|
157
|
-
}
|
|
158
|
-
return terminal.selectedProjectName;
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const getWebAttachSize = useCallback(() => {
|
|
163
|
-
return terminalRef.current?.getSize() ?? { cols: 80, rows: 24 };
|
|
164
|
-
}, []);
|
|
165
|
-
|
|
166
|
-
const attachController = useAttachController({
|
|
167
|
-
flow,
|
|
168
|
-
attachSessionWithBundleRefresh: bundleRefreshAttach.attachSessionWithBundleRefresh,
|
|
169
|
-
defaultProjectName: terminal.selectedProjectName,
|
|
170
|
-
getAttachSize: getWebAttachSize,
|
|
171
|
-
resolveProjectName: (workspaceId) => {
|
|
172
|
-
const index = workspaceId.indexOf(':');
|
|
173
|
-
if (index > 0) {
|
|
174
|
-
return workspaceId.slice(0, index);
|
|
175
|
-
}
|
|
176
|
-
return terminal.selectedProjectName;
|
|
177
|
-
},
|
|
178
|
-
onBeforeAttach: ({ target, params }) => {
|
|
179
|
-
if (target === 'session') {
|
|
180
|
-
setShowScriptTerminal(false);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (params.workspaceId && !params.command) {
|
|
185
|
-
setShowInbox(false);
|
|
186
|
-
setScriptWorkspaceName(params.workspaceId.split(':').slice(-1)[0] ?? params.workspaceId);
|
|
187
|
-
setShowScriptTerminal(true);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
onAttachCancelled: ({ target }) => {
|
|
191
|
-
if (target === 'workspace') {
|
|
192
|
-
setShowScriptTerminal(false);
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
onAttachError: ({ target, message }) => {
|
|
196
|
-
const isWorkspaceScriptFailure = message.startsWith('Workspace scripts failed during');
|
|
197
|
-
const hasScriptRuntimeState = Boolean(terminal.scriptState);
|
|
198
|
-
|
|
199
|
-
if (target === 'workspace' && (!isWorkspaceScriptFailure || !hasScriptRuntimeState)) {
|
|
200
|
-
setShowScriptTerminal(false);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
flow.showMessage({
|
|
204
|
-
title: isWorkspaceScriptFailure ? 'Workspace Script Failed' : 'Session Failed',
|
|
205
|
-
message,
|
|
206
|
-
variant: 'error',
|
|
207
|
-
});
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
212
|
-
flow,
|
|
213
|
-
deleteWorkspace: terminal.deleteWorkspace,
|
|
214
|
-
onBeforeDelete: ({ target }) => {
|
|
215
|
-
suppressDeleteScriptFailureModalRef.current = true;
|
|
216
|
-
setShowInbox(false);
|
|
217
|
-
setScriptWorkspaceName(target.workspaceName);
|
|
218
|
-
setShowScriptTerminal(true);
|
|
219
|
-
},
|
|
220
|
-
onDeleteSuccess: async () => {
|
|
221
|
-
suppressDeleteScriptFailureModalRef.current = false;
|
|
222
|
-
setShowScriptTerminal(false);
|
|
223
|
-
terminal.requestWorkspaces();
|
|
224
|
-
terminal.requestSessions();
|
|
225
|
-
},
|
|
226
|
-
onDeleteCancelled: async () => {
|
|
227
|
-
suppressDeleteScriptFailureModalRef.current = false;
|
|
228
|
-
setShowScriptTerminal(false);
|
|
229
|
-
},
|
|
230
|
-
onDeleteError: async ({ message }) => {
|
|
231
|
-
suppressDeleteScriptFailureModalRef.current = false;
|
|
232
|
-
setShowScriptTerminal(false);
|
|
233
|
-
flow.showMessage({
|
|
234
|
-
title: 'Delete Failed',
|
|
235
|
-
message,
|
|
236
|
-
variant: 'error',
|
|
237
|
-
});
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
useEffect(() => {
|
|
242
|
-
let mounted = true;
|
|
243
|
-
void browserPreferencesService.getNotificationConfig().then((config) => {
|
|
244
|
-
if (mounted) {
|
|
245
|
-
setLocalNotificationConfig(config);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return () => {
|
|
250
|
-
mounted = false;
|
|
251
|
-
};
|
|
252
|
-
}, []);
|
|
253
|
-
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
if (!terminal.notificationConfig) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
void browserPreferencesService.updateNotificationConfig(terminal.notificationConfig);
|
|
259
|
-
setLocalNotificationConfig(terminal.notificationConfig);
|
|
260
|
-
}, [terminal.notificationConfig]);
|
|
261
|
-
|
|
262
|
-
// Parse invite from URL hash on load, and review params from query string
|
|
263
|
-
useEffect(() => {
|
|
264
|
-
const hash = window.location.hash;
|
|
265
|
-
if (hash.startsWith("#invite=")) {
|
|
266
|
-
parseInviteFromHash(hash).then((invite) => {
|
|
267
|
-
if (invite) {
|
|
268
|
-
setInviteParams({
|
|
269
|
-
machineId: invite.machineId,
|
|
270
|
-
inviteId: invite.inviteId,
|
|
271
|
-
inviteToken: invite.inviteToken,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const params = new URLSearchParams(window.location.search);
|
|
278
|
-
if (params.get('view') === 'review') {
|
|
279
|
-
const ws = params.get('workspace');
|
|
280
|
-
const proj = params.get('project');
|
|
281
|
-
if (ws && proj) {
|
|
282
|
-
setReviewWorkspace({ projectName: proj, workspaceId: ws, workspaceLabel: ws });
|
|
283
|
-
setView('review');
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}, []);
|
|
287
|
-
|
|
288
|
-
// Auto-connect on load (no token required for personal relays)
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
if (relay.status === "disconnected") {
|
|
291
|
-
relay.connect();
|
|
292
|
-
}
|
|
293
|
-
}, []);
|
|
294
|
-
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
if (terminal.scriptState?.isRunning) {
|
|
297
|
-
setShowScriptTerminal(true);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (terminal.mode === 'attached' || terminal.status !== 'established') {
|
|
301
|
-
setShowScriptTerminal(false);
|
|
302
|
-
}
|
|
303
|
-
}, [terminal.mode, terminal.scriptState?.isRunning, terminal.status]);
|
|
304
|
-
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
if (
|
|
307
|
-
!pendingProcessEditWorkspaceId ||
|
|
308
|
-
!pendingProcessEditValidationArmedRef.current ||
|
|
309
|
-
terminal.mode !== 'browsing'
|
|
310
|
-
) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
terminal.requestWorkspaces();
|
|
314
|
-
}, [pendingProcessEditWorkspaceId, terminal.mode, terminal.requestWorkspaces]);
|
|
315
|
-
|
|
316
|
-
useEffect(() => {
|
|
317
|
-
if (
|
|
318
|
-
!pendingProcessEditWorkspaceId ||
|
|
319
|
-
!pendingProcessEditValidationArmedRef.current ||
|
|
320
|
-
terminal.mode !== 'browsing'
|
|
321
|
-
) {
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
pendingProcessEditWorkspacesRef.current &&
|
|
327
|
-
pendingProcessEditWorkspacesRef.current === terminal.workspaces
|
|
328
|
-
) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
pendingProcessEditWorkspacesRef.current = null;
|
|
332
|
-
|
|
333
|
-
const workspace = terminal.workspaces.find((item) => item.id === pendingProcessEditWorkspaceId);
|
|
334
|
-
if (!workspace) {
|
|
335
|
-
pendingProcessEditValidationArmedRef.current = false;
|
|
336
|
-
setPendingProcessEditWorkspaceId(null);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (workspace.processConfigError) {
|
|
341
|
-
flow.showMessage({
|
|
342
|
-
title: 'Invalid Processes Config',
|
|
343
|
-
message: workspace.processConfigError,
|
|
344
|
-
variant: 'error',
|
|
345
|
-
});
|
|
346
|
-
} else {
|
|
347
|
-
const processCount = workspace.processes?.length ?? 0;
|
|
348
|
-
flow.showMessage({
|
|
349
|
-
title: 'Processes Config Updated',
|
|
350
|
-
message: processCount === 0
|
|
351
|
-
? 'Config is valid. No processes are defined yet.'
|
|
352
|
-
: `Config is valid. ${processCount} process${processCount === 1 ? '' : 'es'} defined.`,
|
|
353
|
-
variant: 'success',
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
pendingProcessEditValidationArmedRef.current = false;
|
|
358
|
-
setPendingProcessEditWorkspaceId(null);
|
|
359
|
-
}, [flow, pendingProcessEditWorkspaceId, terminal.mode, terminal.workspaces]);
|
|
360
|
-
|
|
361
|
-
useEffect(() => {
|
|
362
|
-
const scriptError = terminal.scriptState?.error;
|
|
363
|
-
if (!scriptError) {
|
|
364
|
-
lastScriptErrorRef.current = null;
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (suppressDeleteScriptFailureModalRef.current) {
|
|
369
|
-
lastScriptErrorRef.current = scriptError;
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (lastScriptErrorRef.current === scriptError) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
lastScriptErrorRef.current = scriptError;
|
|
378
|
-
flow.showMessage({
|
|
379
|
-
title: 'Workspace Script Failed',
|
|
380
|
-
message: scriptError,
|
|
381
|
-
variant: 'error',
|
|
382
|
-
});
|
|
383
|
-
}, [flow, terminal.scriptState?.error]);
|
|
384
|
-
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
if (!terminal.commandError) {
|
|
387
|
-
lastCommandErrorRef.current = null;
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const key = `${terminal.commandError.code ?? ''}:${terminal.commandError.message}`;
|
|
392
|
-
if (lastCommandErrorRef.current === key) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
lastCommandErrorRef.current = key;
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
suppressDeleteScriptFailureModalRef.current &&
|
|
399
|
-
terminal.commandError.code &&
|
|
400
|
-
DELETE_ERROR_CODES.has(terminal.commandError.code)
|
|
401
|
-
) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const isScriptFailure =
|
|
406
|
-
terminal.commandError.code === 'SCRIPT_FAILED' ||
|
|
407
|
-
terminal.commandError.code === 'PRE_SCRIPT_FAILED' ||
|
|
408
|
-
terminal.commandError.code === 'SETUP_SCRIPT_FAILED' ||
|
|
409
|
-
terminal.commandError.code === 'SELECT_SCRIPT_FAILED' ||
|
|
410
|
-
terminal.commandError.code === 'REMOVE_SCRIPT_FAILED';
|
|
411
|
-
|
|
412
|
-
if (isScriptFailure) {
|
|
413
|
-
if (!terminal.scriptState) {
|
|
414
|
-
flow.showMessage({
|
|
415
|
-
title: 'Workspace Script Failed',
|
|
416
|
-
message: terminal.commandError.message,
|
|
417
|
-
variant: 'error',
|
|
418
|
-
});
|
|
419
|
-
setShowScriptTerminal(false);
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (terminal.commandError.code === 'BUNDLE_REFRESH_REQUIRED') {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
flow.showMessage({
|
|
429
|
-
title: 'Session Failed',
|
|
430
|
-
message: terminal.commandError.message,
|
|
431
|
-
variant: 'error',
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
if (terminal.scriptState?.isRunning !== true) {
|
|
435
|
-
setShowScriptTerminal(false);
|
|
436
|
-
}
|
|
437
|
-
}, [flow, terminal.commandError, terminal.scriptState?.isRunning]);
|
|
438
|
-
|
|
439
|
-
// Copy access command to clipboard
|
|
440
|
-
const copyAccessCommand = async () => {
|
|
441
|
-
if (relay.publicKey) {
|
|
442
|
-
const command = `gssh access add "${relay.publicKey}"`;
|
|
443
|
-
await navigator.clipboard.writeText(command);
|
|
444
|
-
setCopied(true);
|
|
445
|
-
setTimeout(() => setCopied(false), 2000);
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
// Handle machine selection - go directly to terminal/workspaces view
|
|
450
|
-
const handleMachineConnect = async (machine: MachineInfo) => {
|
|
451
|
-
if (!machine.online) return;
|
|
452
|
-
|
|
453
|
-
// Get WebSocket and identity from relay connection
|
|
454
|
-
const ws = relay.getWebSocket();
|
|
455
|
-
const identity = relay.identity;
|
|
456
|
-
if (!ws || !identity) {
|
|
457
|
-
console.error("No WebSocket or identity available");
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
setSelectedMachine(machine);
|
|
462
|
-
setView("terminal");
|
|
463
|
-
setShowScriptTerminal(false);
|
|
464
|
-
setScriptWorkspaceName('workspace');
|
|
465
|
-
|
|
466
|
-
// Connect to the machine using existing WebSocket (no new connection needed)
|
|
467
|
-
await terminal.connect({
|
|
468
|
-
ws,
|
|
469
|
-
identity,
|
|
470
|
-
machineId: machine.machineId,
|
|
471
|
-
inviteId: inviteParams?.inviteId,
|
|
472
|
-
inviteToken: inviteParams?.inviteToken,
|
|
473
|
-
});
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
// Handle back to machine list
|
|
477
|
-
const handleBackToMachines = () => {
|
|
478
|
-
terminal.disconnect();
|
|
479
|
-
setSelectedMachine(null);
|
|
480
|
-
setShowScriptTerminal(false);
|
|
481
|
-
setShowEvents(false);
|
|
482
|
-
setInputMode(false); // Reset input mode when leaving terminal
|
|
483
|
-
setView("machines");
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
// Handle full disconnect (just refresh the page for simplicity)
|
|
487
|
-
const handleDisconnect = () => {
|
|
488
|
-
terminal.disconnect();
|
|
489
|
-
relay.disconnect();
|
|
490
|
-
setSelectedMachine(null);
|
|
491
|
-
setShowScriptTerminal(false);
|
|
492
|
-
setShowEvents(false);
|
|
493
|
-
setView("machines");
|
|
494
|
-
// Reconnect automatically
|
|
495
|
-
relay.connect();
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
// ========== Shared Hooks ==========
|
|
499
|
-
|
|
500
|
-
// Machine list hook - convert relay machines to shared MachineInfo format
|
|
501
|
-
const machineListProps = useMachineList({
|
|
502
|
-
machines: relay.machines,
|
|
503
|
-
status: relay.status,
|
|
504
|
-
error: relay.error,
|
|
505
|
-
publicKey: relay.publicKey,
|
|
506
|
-
onConnect: handleMachineConnect,
|
|
507
|
-
onRefresh: relay.refreshMachines,
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Handle attach session - show modal for new sessions
|
|
511
|
-
const handleAttachSession = useCallback(async (params: { sessionId?: string; workspaceId?: string; viewOnly?: boolean }) => {
|
|
512
|
-
setIsViewOnlySession(params.viewOnly ?? false);
|
|
513
|
-
await attachController.attachFromSelection(params);
|
|
514
|
-
}, [attachController]);
|
|
515
|
-
|
|
516
|
-
const processActions = useProcessActions({
|
|
517
|
-
sessions: terminal.sessions,
|
|
518
|
-
startProcess: terminal.startProcess,
|
|
519
|
-
stopProcess: terminal.stopProcess,
|
|
520
|
-
attachSession: handleAttachSession,
|
|
521
|
-
onStartProcessError: (error) => {
|
|
522
|
-
toast.error(error instanceof Error ? error.message : String(error));
|
|
523
|
-
},
|
|
524
|
-
onStopProcessError: (error) => {
|
|
525
|
-
toast.error(error instanceof Error ? error.message : String(error));
|
|
526
|
-
},
|
|
527
|
-
onStartProcessAttachError: (error) => {
|
|
528
|
-
toast.error(error instanceof Error ? error.message : String(error));
|
|
529
|
-
},
|
|
530
|
-
onAttachError: (error) => {
|
|
531
|
-
toast.error(error instanceof Error ? error.message : String(error));
|
|
532
|
-
},
|
|
533
|
-
onAttachTimeout: (target) => {
|
|
534
|
-
toast.error(`Process started but no active session appeared for ${target.processName}#${target.instance}.`);
|
|
535
|
-
},
|
|
536
|
-
pendingAttachCancelSignal: terminal.commandError,
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Handle opening review for a workspace
|
|
540
|
-
const handleOpenReview = useCallback((workspace: WorkspaceInfo) => {
|
|
541
|
-
setReviewWorkspace({
|
|
542
|
-
projectName: workspace.projectName,
|
|
543
|
-
workspaceId: workspace.id,
|
|
544
|
-
workspaceLabel: workspace.name,
|
|
545
|
-
});
|
|
546
|
-
setView('review');
|
|
547
|
-
}, []);
|
|
548
|
-
|
|
549
|
-
// Open editor on .gitspace/processes.json in the workspace
|
|
550
|
-
const handleEditProcesses = useCallback(({ workspaceId }: { workspaceId: string }) => {
|
|
551
|
-
setIsViewOnlySession(false);
|
|
552
|
-
pendingProcessEditValidationArmedRef.current = false;
|
|
553
|
-
pendingProcessEditWorkspacesRef.current = terminal.workspaces;
|
|
554
|
-
setPendingProcessEditWorkspaceId(workspaceId);
|
|
555
|
-
const commandSpec = buildEditProcessesCommand();
|
|
556
|
-
void attachController.attach({
|
|
557
|
-
workspaceId,
|
|
558
|
-
command: commandSpec.command,
|
|
559
|
-
args: commandSpec.args,
|
|
560
|
-
}).then((attached) => {
|
|
561
|
-
if (!attached) {
|
|
562
|
-
pendingProcessEditValidationArmedRef.current = false;
|
|
563
|
-
pendingProcessEditWorkspacesRef.current = null;
|
|
564
|
-
setPendingProcessEditWorkspaceId(null);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
pendingProcessEditValidationArmedRef.current = true;
|
|
569
|
-
});
|
|
570
|
-
}, [attachController, terminal.workspaces]);
|
|
571
|
-
|
|
572
|
-
const handleProcessDisabled = useCallback((params: { workspaceId: string; processName: string }) => {
|
|
573
|
-
const workspace = terminal.workspaces.find((item) => item.id === params.workspaceId);
|
|
574
|
-
const workspaceLabel = workspace?.name ?? params.workspaceId;
|
|
575
|
-
toast.error(`Process "${params.processName}" is disabled in ${workspaceLabel} (instances: 0).`);
|
|
576
|
-
}, [terminal.workspaces]);
|
|
577
|
-
|
|
578
|
-
// Spaces browser hook
|
|
579
|
-
const spacesBrowserProps = useSpacesBrowser({
|
|
580
|
-
workspaces: terminal.workspaces,
|
|
581
|
-
sessions: terminal.sessions,
|
|
582
|
-
onRequestSessions: () => terminal.requestSessions(),
|
|
583
|
-
onAttachSession: handleAttachSession,
|
|
584
|
-
onEditProcesses: handleEditProcesses,
|
|
585
|
-
onStartProcess: (params) => processActions.handleStartProcess(params),
|
|
586
|
-
onStartProcessAttach: (params) => processActions.handleStartProcessAttach(params),
|
|
587
|
-
onStopProcess: (params) => processActions.handleStopProcess(params),
|
|
588
|
-
onProcessDisabled: handleProcessDisabled,
|
|
589
|
-
onOpenEvents: (workspaceId) => {
|
|
590
|
-
const workspace = terminal.workspaces.find(w => w.id === workspaceId);
|
|
591
|
-
if (workspace) {
|
|
592
|
-
setEventsWorkspacePath(workspace.path);
|
|
593
|
-
setEventsWorkspaceLabel(workspace.name);
|
|
594
|
-
setShowEvents(true);
|
|
595
|
-
terminal.requestEvents(workspace.path, undefined, undefined, undefined);
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
onRefresh: terminal.requestWorkspaces,
|
|
599
|
-
onRefreshSessions: () => terminal.requestSessions(),
|
|
600
|
-
onBack: handleBackToMachines,
|
|
601
|
-
machineName: selectedMachine?.label || selectedMachine?.machineId,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// Inbox hook
|
|
605
|
-
const inboxProps = useInbox({
|
|
606
|
-
items: terminal.inbox,
|
|
607
|
-
unreadCount: terminal.inboxUnreadCount,
|
|
608
|
-
onClearItem: async (id) => terminal.clearInboxItem(id),
|
|
609
|
-
onClearAll: async () => terminal.clearInboxItem(),
|
|
610
|
-
onMarkRead: async (id) => terminal.markInboxItemRead(id),
|
|
611
|
-
onAttachSession: async (sessionId) => {
|
|
612
|
-
setShowInbox(false);
|
|
613
|
-
await attachController.attach({ sessionId });
|
|
614
|
-
},
|
|
615
|
-
onClose: () => setShowInbox(false),
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// Events hook
|
|
619
|
-
const eventsItems: WideEventItem[] = terminal.events.map(toWideEventItem);
|
|
620
|
-
|
|
621
|
-
const eventsProps = useEvents({
|
|
622
|
-
events: eventsItems,
|
|
623
|
-
liveEventIds: terminal.liveEventIds,
|
|
624
|
-
savedFilters: terminal.savedEventFilters,
|
|
625
|
-
onSelectFilter: (filter) => {
|
|
626
|
-
if (!eventsWorkspacePath) return;
|
|
627
|
-
if (filter) {
|
|
628
|
-
const sinceMs = filter.sinceMinutes
|
|
629
|
-
? Date.now() - filter.sinceMinutes * 60 * 1000
|
|
630
|
-
: undefined;
|
|
631
|
-
terminal.requestEvents(
|
|
632
|
-
eventsWorkspacePath,
|
|
633
|
-
filter.filter as WideEventFilter,
|
|
634
|
-
undefined,
|
|
635
|
-
sinceMs
|
|
636
|
-
);
|
|
637
|
-
} else {
|
|
638
|
-
terminal.requestEvents(eventsWorkspacePath);
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
onClose: () => {
|
|
642
|
-
setShowEvents(false);
|
|
643
|
-
setEventsWorkspacePath(null);
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
useEffect(() => {
|
|
648
|
-
eventsKeyboardStateRef.current = {
|
|
649
|
-
selectedIndex: eventsProps.selectedIndex,
|
|
650
|
-
selectIndex: eventsProps.selectIndex,
|
|
651
|
-
};
|
|
652
|
-
}, [eventsProps.selectedIndex, eventsProps.selectIndex]);
|
|
653
|
-
|
|
654
|
-
// Events polling when events view is active
|
|
655
|
-
useEffect(() => {
|
|
656
|
-
if (!showEvents || !eventsWorkspacePath) return;
|
|
657
|
-
|
|
658
|
-
const interval = setInterval(() => {
|
|
659
|
-
const activeFilter = eventsProps.activeFilterName
|
|
660
|
-
? terminal.savedEventFilters.find((filter) => filter.name === eventsProps.activeFilterName) ?? null
|
|
661
|
-
: null;
|
|
662
|
-
|
|
663
|
-
if (activeFilter) {
|
|
664
|
-
const sinceMs = activeFilter.sinceMinutes
|
|
665
|
-
? Date.now() - activeFilter.sinceMinutes * 60 * 1000
|
|
666
|
-
: undefined;
|
|
667
|
-
terminal.requestEvents(
|
|
668
|
-
eventsWorkspacePath,
|
|
669
|
-
activeFilter.filter as WideEventFilter,
|
|
670
|
-
undefined,
|
|
671
|
-
sinceMs
|
|
672
|
-
);
|
|
673
|
-
} else {
|
|
674
|
-
terminal.requestEvents(eventsWorkspacePath);
|
|
675
|
-
}
|
|
676
|
-
}, 2000);
|
|
677
|
-
return () => clearInterval(interval);
|
|
678
|
-
}, [showEvents, eventsWorkspacePath, eventsProps.activeFilterName, terminal.savedEventFilters, terminal.requestEvents]);
|
|
679
|
-
|
|
680
|
-
// ========== Activity Tracking for Notifications ==========
|
|
681
|
-
|
|
682
|
-
const holdWhenIdleMs = activeNotificationConfig.toast.holdWhenIdleMs ?? 15000;
|
|
683
|
-
const { isUserActive, markActivity: handleTerminalActivity } = useUserActivity({
|
|
684
|
-
isActivityTracked: view === "terminal" && terminal.mode === "attached",
|
|
685
|
-
holdWhenIdleMs,
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// ========== Notification Toasts ==========
|
|
689
|
-
|
|
690
|
-
const handleShowToast = useCallback((notification: ToastNotification) => {
|
|
691
|
-
const description = notification.preview
|
|
692
|
-
? `${notification.preview} · Shift+Tab to attach`
|
|
693
|
-
: 'Shift+Tab to attach';
|
|
694
|
-
toast(notification.title, {
|
|
695
|
-
description,
|
|
696
|
-
icon: notification.icon,
|
|
697
|
-
duration: 8000,
|
|
698
|
-
action: {
|
|
699
|
-
label: "Attach",
|
|
700
|
-
onClick: () => {
|
|
701
|
-
void attachController.attach({ sessionId: notification.sessionId });
|
|
702
|
-
},
|
|
703
|
-
},
|
|
704
|
-
});
|
|
705
|
-
}, [attachController]);
|
|
706
|
-
|
|
707
|
-
const notifications = useNotifications({
|
|
708
|
-
items: terminal.inbox,
|
|
709
|
-
config: activeNotificationConfig,
|
|
710
|
-
onShowToast: handleShowToast,
|
|
711
|
-
onAttachSession: (sessionId) => {
|
|
712
|
-
void attachController.attach({ sessionId });
|
|
713
|
-
},
|
|
714
|
-
onMarkRead: async (itemId) => {
|
|
715
|
-
terminal.markInboxItemRead(itemId);
|
|
716
|
-
},
|
|
717
|
-
pollIntervalMs: 5000,
|
|
718
|
-
onRefreshInbox: async () => {
|
|
719
|
-
if (terminal.status === "established") {
|
|
720
|
-
terminal.requestInbox();
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
isUserActive,
|
|
724
|
-
currentSessionId: terminal.attachedSessionId ?? undefined,
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
// Request workspaces when connection is established and view is "terminal"
|
|
728
|
-
useEffect(() => {
|
|
729
|
-
if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
|
|
730
|
-
terminal.requestProjects();
|
|
731
|
-
terminal.requestWorkspaces();
|
|
732
|
-
terminal.requestSessions();
|
|
733
|
-
terminal.requestNotificationConfig();
|
|
734
|
-
}
|
|
735
|
-
}, [
|
|
736
|
-
view,
|
|
737
|
-
terminal.status,
|
|
738
|
-
terminal.mode,
|
|
739
|
-
terminal.requestProjects,
|
|
740
|
-
terminal.requestWorkspaces,
|
|
741
|
-
terminal.requestSessions,
|
|
742
|
-
terminal.requestNotificationConfig,
|
|
743
|
-
]);
|
|
744
|
-
|
|
745
|
-
// Reset view-only state when detached
|
|
746
|
-
useEffect(() => {
|
|
747
|
-
if (terminal.mode !== 'attached') {
|
|
748
|
-
setIsViewOnlySession(false);
|
|
749
|
-
}
|
|
750
|
-
}, [terminal.mode]);
|
|
751
|
-
|
|
752
|
-
// ========== Keyboard Handlers ==========
|
|
753
|
-
|
|
754
|
-
// Machine list keyboard navigation
|
|
755
|
-
useEffect(() => {
|
|
756
|
-
if (view !== "machines") return;
|
|
757
|
-
|
|
758
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
759
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const command = resolveMachineListCommand({ key: e.key, shift: e.shiftKey });
|
|
764
|
-
if (!command) {
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
e.preventDefault();
|
|
769
|
-
if (command === 'move-up') {
|
|
770
|
-
machineListProps.moveUp();
|
|
771
|
-
} else if (command === 'move-down') {
|
|
772
|
-
machineListProps.moveDown();
|
|
773
|
-
} else if (command === 'activate') {
|
|
774
|
-
machineListProps.connectSelected();
|
|
775
|
-
} else if (command === 'refresh') {
|
|
776
|
-
void machineListProps.refresh();
|
|
777
|
-
} else if (command === 'copy') {
|
|
778
|
-
machineListProps.copyPublicKey();
|
|
779
|
-
} else if (command === 'help') {
|
|
780
|
-
flow.showHelp(getDefaultShortcuts());
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
785
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
786
|
-
}, [view, machineListProps, flow]);
|
|
787
|
-
|
|
788
|
-
// Inbox keyboard navigation
|
|
789
|
-
useEffect(() => {
|
|
790
|
-
if (!showInbox) return;
|
|
791
|
-
|
|
792
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
793
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const command = resolveInboxCommand({ key: e.key, shift: e.shiftKey });
|
|
798
|
-
if (!command) {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
e.preventDefault();
|
|
803
|
-
if (command === 'move-up') {
|
|
804
|
-
inboxProps.moveUp();
|
|
805
|
-
} else if (command === 'move-down') {
|
|
806
|
-
inboxProps.moveDown();
|
|
807
|
-
} else if (command === 'activate') {
|
|
808
|
-
if (inboxProps.isViewingThread) {
|
|
809
|
-
inboxProps.attachToSession();
|
|
810
|
-
} else {
|
|
811
|
-
inboxProps.openThread();
|
|
812
|
-
}
|
|
813
|
-
} else if (command === 'back') {
|
|
814
|
-
if (inboxProps.isViewingThread) {
|
|
815
|
-
inboxProps.closeThread();
|
|
816
|
-
} else {
|
|
817
|
-
setShowInbox(false);
|
|
818
|
-
}
|
|
819
|
-
} else if (command === 'delete') {
|
|
820
|
-
if (inboxProps.isViewingThread) {
|
|
821
|
-
inboxProps.deleteThread();
|
|
822
|
-
} else {
|
|
823
|
-
inboxProps.deleteSelected();
|
|
824
|
-
}
|
|
825
|
-
} else if (command === 'clear') {
|
|
826
|
-
inboxProps.clearAll();
|
|
827
|
-
} else if (command === 'attach') {
|
|
828
|
-
inboxProps.attachToSession();
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
833
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
834
|
-
}, [showInbox, inboxProps.moveUp, inboxProps.moveDown, inboxProps.openThread, inboxProps.closeThread, inboxProps.deleteSelected, inboxProps.deleteThread, inboxProps.clearAll, inboxProps.attachToSession, inboxProps.isViewingThread]);
|
|
835
|
-
|
|
836
|
-
// Events keyboard navigation
|
|
837
|
-
useEffect(() => {
|
|
838
|
-
if (!showEvents) return;
|
|
839
|
-
|
|
840
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
841
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (e.key === 'Escape' || e.key === 'q') {
|
|
846
|
-
e.preventDefault();
|
|
847
|
-
setShowEvents(false);
|
|
848
|
-
setEventsWorkspacePath(null);
|
|
849
|
-
} else if (e.key === 'ArrowUp' || e.key === 'k') {
|
|
850
|
-
e.preventDefault();
|
|
851
|
-
const state = eventsKeyboardStateRef.current;
|
|
852
|
-
if (!state) return;
|
|
853
|
-
state.selectIndex(state.selectedIndex - 1);
|
|
854
|
-
} else if (e.key === 'ArrowDown' || e.key === 'j') {
|
|
855
|
-
e.preventDefault();
|
|
856
|
-
const state = eventsKeyboardStateRef.current;
|
|
857
|
-
if (!state) return;
|
|
858
|
-
state.selectIndex(state.selectedIndex + 1);
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
863
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
864
|
-
}, [showEvents]);
|
|
865
|
-
|
|
866
|
-
// Spaces browser keyboard navigation
|
|
867
|
-
useEffect(() => {
|
|
868
|
-
if (view !== "terminal" || terminal.status !== "established" || terminal.mode !== "browsing" || showInbox || showScriptTerminal || showEvents) {
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
873
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const command = resolveSessionBrowserCommand({ key: e.key, shift: e.shiftKey });
|
|
878
|
-
if (!command) {
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
e.preventDefault();
|
|
883
|
-
if (command === 'move-up') {
|
|
884
|
-
spacesBrowserProps.moveUp();
|
|
885
|
-
} else if (command === 'move-down') {
|
|
886
|
-
spacesBrowserProps.moveDown();
|
|
887
|
-
} else if (command === 'activate') {
|
|
888
|
-
spacesBrowserProps.activateSelected();
|
|
889
|
-
} else if (command === 'new') {
|
|
890
|
-
spacesBrowserProps.createNewSession();
|
|
891
|
-
} else if (command === 'refresh') {
|
|
892
|
-
spacesBrowserProps.refresh();
|
|
893
|
-
} else if (command === 'back') {
|
|
894
|
-
spacesBrowserProps.back();
|
|
895
|
-
} else if (command === 'help') {
|
|
896
|
-
flow.showHelp(getDefaultShortcuts());
|
|
897
|
-
} else if (command === 'kill') {
|
|
898
|
-
const selected = spacesBrowserProps.selectedItem;
|
|
899
|
-
if (selected?.type === 'session') {
|
|
900
|
-
flow.showConfirm({
|
|
901
|
-
title: 'Kill Session',
|
|
902
|
-
message: `Kill session "${selected.session.name}"?`,
|
|
903
|
-
variant: 'warning',
|
|
904
|
-
confirmLabel: 'Kill',
|
|
905
|
-
onConfirm: () => {
|
|
906
|
-
terminal.killSession(selected.session.id);
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
} else if (selected?.type === 'process' && selected.status === 'running') {
|
|
910
|
-
flow.showConfirm({
|
|
911
|
-
title: 'Stop Process',
|
|
912
|
-
message: `Stop process "${selected.processName}"?`,
|
|
913
|
-
variant: 'warning',
|
|
914
|
-
confirmLabel: 'Stop',
|
|
915
|
-
onConfirm: () => {
|
|
916
|
-
processActions.handleStopProcess({
|
|
917
|
-
workspaceId: selected.workspaceId,
|
|
918
|
-
processName: selected.processName,
|
|
919
|
-
});
|
|
920
|
-
},
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
} else if (command === 'delete') {
|
|
924
|
-
const selected = spacesBrowserProps.selectedItem;
|
|
925
|
-
if (selected?.type === 'workspace') {
|
|
926
|
-
const sessionCount = selected.workspace.sessionCount || 0;
|
|
927
|
-
flow.showConfirmTyped({
|
|
928
|
-
title: 'Delete Workspace',
|
|
929
|
-
message: `Are you sure you want to delete workspace "${selected.workspace.name}"?`,
|
|
930
|
-
confirmText: selected.workspace.name,
|
|
931
|
-
warning: sessionCount > 0 ? `This will kill ${sessionCount} active session(s)!` : undefined,
|
|
932
|
-
onConfirm: async () => {
|
|
933
|
-
await deleteWorkspaceWithPrompt({
|
|
934
|
-
projectName: selected.workspace.projectName,
|
|
935
|
-
workspaceId: selected.workspace.id,
|
|
936
|
-
workspaceName: selected.workspace.name,
|
|
937
|
-
});
|
|
938
|
-
},
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
} else if (command === 'open-inbox') {
|
|
942
|
-
terminal.requestInbox();
|
|
943
|
-
setShowInbox(true);
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
948
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
949
|
-
}, [
|
|
950
|
-
view,
|
|
951
|
-
terminal.status,
|
|
952
|
-
terminal.mode,
|
|
953
|
-
showInbox,
|
|
954
|
-
showScriptTerminal,
|
|
955
|
-
showEvents,
|
|
956
|
-
spacesBrowserProps,
|
|
957
|
-
flow,
|
|
958
|
-
deleteWorkspaceWithPrompt,
|
|
959
|
-
]);
|
|
960
|
-
|
|
961
|
-
// Attached terminal mode keyboard handler (Ctrl+Esc to detach)
|
|
962
|
-
useEffect(() => {
|
|
963
|
-
if (view !== "terminal" || terminal.status !== "established" || terminal.mode !== "attached") {
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
968
|
-
// Ctrl+Esc to detach from session
|
|
969
|
-
if (e.ctrlKey && e.key === "Escape") {
|
|
970
|
-
e.preventDefault();
|
|
971
|
-
terminal.detachSession();
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
|
|
975
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
976
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
977
|
-
}, [view, terminal.status, terminal.mode, terminal.detachSession]);
|
|
978
|
-
|
|
979
|
-
useEffect(() => {
|
|
980
|
-
if (view !== 'terminal' || terminal.status !== 'established' || terminal.mode !== 'browsing' || !showScriptTerminal) {
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
985
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if ((e.key === 'Escape' || e.key === 'q') && !terminal.scriptState?.isRunning) {
|
|
990
|
-
e.preventDefault();
|
|
991
|
-
setShowScriptTerminal(false);
|
|
992
|
-
}
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
996
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
997
|
-
}, [showScriptTerminal, terminal.mode, terminal.scriptState?.isRunning, terminal.status, view]);
|
|
998
|
-
|
|
999
|
-
// Global hotkey for toast-only attach (Shift+Tab)
|
|
1000
|
-
useEffect(() => {
|
|
1001
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
1002
|
-
// Skip if in an input field
|
|
1003
|
-
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Shift+Tab: attach to active toast's session (with confirmation)
|
|
1008
|
-
if (e.shiftKey && e.key === "Tab" && notifications.activeToast) {
|
|
1009
|
-
e.preventDefault();
|
|
1010
|
-
const sessionLabel = getSessionLabel(notifications.activeToast.sessionName);
|
|
1011
|
-
flow.showConfirm({
|
|
1012
|
-
title: 'Switch Session',
|
|
1013
|
-
message: `Switch to "${sessionLabel}"?`,
|
|
1014
|
-
confirmLabel: 'Switch',
|
|
1015
|
-
onConfirm: () => {
|
|
1016
|
-
notifications.attachToActiveToast();
|
|
1017
|
-
},
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
1023
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1024
|
-
}, [notifications.activeToast, notifications.attachToActiveToast, flow]);
|
|
1025
|
-
|
|
1026
|
-
// ========== Review View ==========
|
|
1027
|
-
if (view === 'review' && reviewWorkspace) {
|
|
1028
|
-
if (terminal.status === 'established') {
|
|
1029
|
-
return (
|
|
1030
|
-
<>
|
|
1031
|
-
<ReviewPage
|
|
1032
|
-
projectName={reviewWorkspace.projectName}
|
|
1033
|
-
workspaceName={reviewWorkspace.workspaceId}
|
|
1034
|
-
workspaceLabel={reviewWorkspace.workspaceLabel}
|
|
1035
|
-
machineName={selectedMachine?.label || selectedMachine?.machineId}
|
|
1036
|
-
sendReviewRequest={terminal.sendReviewRequest}
|
|
1037
|
-
onBack={() => {
|
|
1038
|
-
setView('terminal');
|
|
1039
|
-
setReviewWorkspace(null);
|
|
1040
|
-
}}
|
|
1041
|
-
/>
|
|
1042
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1043
|
-
</>
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Connection not yet established — show a targeted connecting screen
|
|
1048
|
-
// rather than falling through to the generic machine list.
|
|
1049
|
-
const statusMessage = {
|
|
1050
|
-
disconnected: "Disconnected",
|
|
1051
|
-
connecting: "Connecting to relay...",
|
|
1052
|
-
connected: "Connected, authenticating...",
|
|
1053
|
-
handshaking: "Establishing secure connection...",
|
|
1054
|
-
established: "Connected!",
|
|
1055
|
-
error: "Connection failed",
|
|
1056
|
-
}[terminal.status];
|
|
1057
|
-
|
|
1058
|
-
return (
|
|
1059
|
-
<>
|
|
1060
|
-
<div className="h-screen w-screen flex flex-col items-center justify-center bg-[#0d1117] px-4">
|
|
1061
|
-
<div className="text-center">
|
|
1062
|
-
<div className="text-lg text-[#e6edf3] mb-2">
|
|
1063
|
-
Loading review for <span className="text-[#58a6ff]">{reviewWorkspace.workspaceLabel ?? reviewWorkspace.workspaceId}</span>
|
|
1064
|
-
</div>
|
|
1065
|
-
<div className="text-sm text-[#8b949e]">{statusMessage}</div>
|
|
1066
|
-
<button
|
|
1067
|
-
onClick={() => { setView('machines'); setReviewWorkspace(null); }}
|
|
1068
|
-
className="mt-4 px-6 py-3 text-base bg-[#21262d] hover:bg-[#30363d] rounded-lg text-[#e6edf3] min-h-[48px] border border-[#30363d]"
|
|
1069
|
-
>
|
|
1070
|
-
Back to Machines
|
|
1071
|
-
</button>
|
|
1072
|
-
</div>
|
|
1073
|
-
</div>
|
|
1074
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1075
|
-
</>
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// ========== Spaces Browser View (browsing mode) ==========
|
|
1080
|
-
if (
|
|
1081
|
-
view === 'terminal' &&
|
|
1082
|
-
terminal.status === 'established' &&
|
|
1083
|
-
terminal.mode === 'browsing' &&
|
|
1084
|
-
showScriptTerminal
|
|
1085
|
-
) {
|
|
1086
|
-
const isRunning = terminal.scriptState?.isRunning ?? true;
|
|
1087
|
-
return (
|
|
1088
|
-
<>
|
|
1089
|
-
<ScriptTerminal
|
|
1090
|
-
phase={terminal.scriptState?.phase ?? 'pre'}
|
|
1091
|
-
workspaceName={scriptWorkspaceName}
|
|
1092
|
-
isRunning={isRunning}
|
|
1093
|
-
error={terminal.scriptState?.error}
|
|
1094
|
-
exitCode={terminal.scriptState?.exitCode}
|
|
1095
|
-
setWriteCallback={terminal.setWriteCallback}
|
|
1096
|
-
onBack={() => setShowScriptTerminal(false)}
|
|
1097
|
-
/>
|
|
1098
|
-
{!isRunning && <FlowWeb flow={flow} />}
|
|
1099
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1100
|
-
</>
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
|
|
1105
|
-
// Show inbox if open
|
|
1106
|
-
if (showInbox) {
|
|
1107
|
-
return (
|
|
1108
|
-
<>
|
|
1109
|
-
<InboxWeb {...inboxProps} />
|
|
1110
|
-
<FlowWeb flow={flow} />
|
|
1111
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1112
|
-
</>
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// Show events if open
|
|
1117
|
-
if (showEvents) {
|
|
1118
|
-
return (
|
|
1119
|
-
<>
|
|
1120
|
-
<EventsWeb {...eventsProps} workspaceLabel={eventsWorkspaceLabel} />
|
|
1121
|
-
<FlowWeb flow={flow} />
|
|
1122
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1123
|
-
</>
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
return (
|
|
1128
|
-
<>
|
|
1129
|
-
<div className="h-screen w-screen flex flex-col bg-[#0d1117]">
|
|
1130
|
-
<div className="bg-[#161b22] px-4 py-2 flex items-center justify-between border-b border-[#30363d] min-h-[52px] gap-2">
|
|
1131
|
-
<div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
|
|
1132
|
-
<button
|
|
1133
|
-
onClick={handleBackToMachines}
|
|
1134
|
-
className="text-sm text-[#8b949e] hover:text-[#e6edf3] active:text-[#22c55e] py-2 pr-2 -ml-2 min-h-[44px] flex items-center flex-shrink-0"
|
|
1135
|
-
>
|
|
1136
|
-
← <span className="hidden sm:inline ml-1">Machines</span>
|
|
1137
|
-
</button>
|
|
1138
|
-
<div className="text-sm text-[#8b949e] truncate hidden sm:block">
|
|
1139
|
-
<span className="text-[#3fb950] shadow-glow">●</span>{" "}
|
|
1140
|
-
{selectedMachine?.label || selectedMachine?.machineId}
|
|
1141
|
-
</div>
|
|
1142
|
-
</div>
|
|
1143
|
-
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
|
|
1144
|
-
<button
|
|
1145
|
-
onClick={() => {
|
|
1146
|
-
terminal.requestInbox();
|
|
1147
|
-
setShowInbox(true);
|
|
1148
|
-
}}
|
|
1149
|
-
className="text-sm text-[#8b949e] hover:text-[#e6edf3] active:text-[#22c55e] flex items-center gap-1 py-2 px-2 min-h-[44px]"
|
|
1150
|
-
>
|
|
1151
|
-
<span className="hidden sm:inline text-xs text-[#6e7681]">[i]</span>
|
|
1152
|
-
<span>Inbox</span>
|
|
1153
|
-
{terminal.inboxUnreadCount > 0 && (
|
|
1154
|
-
<span className="ml-1 px-1.5 py-0.5 text-xs bg-[#58a6ff] rounded-full text-[#0d1117] font-medium">
|
|
1155
|
-
{terminal.inboxUnreadCount}
|
|
1156
|
-
</span>
|
|
1157
|
-
)}
|
|
1158
|
-
</button>
|
|
1159
|
-
<button
|
|
1160
|
-
onClick={handleDisconnect}
|
|
1161
|
-
className="px-3 py-2 text-sm bg-[#f85149] hover:bg-[#ff7b72] active:bg-[#da3633] rounded text-white min-h-[44px] border border-[#f85149]"
|
|
1162
|
-
>
|
|
1163
|
-
<span className="hidden sm:inline">Disconnect</span>
|
|
1164
|
-
<span className="sm:hidden">×</span>
|
|
1165
|
-
</button>
|
|
1166
|
-
</div>
|
|
1167
|
-
</div>
|
|
1168
|
-
<div className="flex-1 overflow-hidden">
|
|
1169
|
-
<SpacesBrowserWeb {...spacesBrowserProps} onReview={handleOpenReview} />
|
|
1170
|
-
</div>
|
|
1171
|
-
</div>
|
|
1172
|
-
<FlowWeb flow={flow} />
|
|
1173
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1174
|
-
</>
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// ========== Terminal View (attached mode) ==========
|
|
1179
|
-
if (view === "terminal" && terminal.status === "established" && terminal.mode === "attached") {
|
|
1180
|
-
// Handler for sending data from mobile controls (already processed)
|
|
1181
|
-
const handleSendData = (data: string) => {
|
|
1182
|
-
if (data === PAGE_UP && terminalRef.current?.pageUp()) {
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
if (data === PAGE_DOWN && terminalRef.current?.pageDown()) {
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
if (isViewOnlySession) {
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
terminal.send(new TextEncoder().encode(data));
|
|
1195
|
-
};
|
|
1196
|
-
|
|
1197
|
-
// Handler for keyboard input - applies virtual modifiers then resets them
|
|
1198
|
-
const handleKeyboardData = (data: Uint8Array) => {
|
|
1199
|
-
if (isViewOnlySession) {
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const hasModifiers = modifiers.ctrl || modifiers.shift || modifiers.alt;
|
|
1204
|
-
if (hasModifiers) {
|
|
1205
|
-
// Apply modifiers and reset
|
|
1206
|
-
const modified = applyModifiersToInput(data, modifiers);
|
|
1207
|
-
terminal.send(modified);
|
|
1208
|
-
setModifiers({ ctrl: false, shift: false, alt: false });
|
|
1209
|
-
} else {
|
|
1210
|
-
terminal.send(data);
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
// Handler for focusing terminal from mobile controls
|
|
1215
|
-
const handleFocusTerminal = () => {
|
|
1216
|
-
terminalRef.current?.focus();
|
|
1217
|
-
};
|
|
1218
|
-
|
|
1219
|
-
// Toggle input mode - focus/blur terminal accordingly
|
|
1220
|
-
const toggleInputMode = () => {
|
|
1221
|
-
const newInputMode = !inputMode;
|
|
1222
|
-
setInputMode(newInputMode);
|
|
1223
|
-
if (newInputMode) {
|
|
1224
|
-
// Entering input mode - focus terminal to show keyboard
|
|
1225
|
-
terminalRef.current?.focus();
|
|
1226
|
-
} else {
|
|
1227
|
-
// Exiting input mode - blur to hide keyboard
|
|
1228
|
-
terminalRef.current?.blur();
|
|
1229
|
-
}
|
|
1230
|
-
};
|
|
1231
|
-
|
|
1232
|
-
// Show floating controls when keyboard is hidden on mobile
|
|
1233
|
-
const showFloatingControls = showMobileControls && !keyboardVisible;
|
|
1234
|
-
|
|
1235
|
-
// Determine terminal container classes based on mode
|
|
1236
|
-
const getTerminalContainerClass = () => {
|
|
1237
|
-
if (!showMobileControls) {
|
|
1238
|
-
// Desktop - simple flex
|
|
1239
|
-
return 'flex-1';
|
|
1240
|
-
}
|
|
1241
|
-
if (inputMode) {
|
|
1242
|
-
// Input mode - has padding for TerminalControls bar
|
|
1243
|
-
return 'terminal-input-mode-container';
|
|
1244
|
-
}
|
|
1245
|
-
// Not input mode - add bottom padding for floating controls
|
|
1246
|
-
return 'flex-1 terminal-with-floating-controls';
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
return (
|
|
1250
|
-
<>
|
|
1251
|
-
<div className="w-screen h-screen flex flex-col bg-[#0d1117] overflow-hidden">
|
|
1252
|
-
<div className="bg-[#161b22] px-4 py-2 flex items-center justify-between border-b border-[#30363d] min-h-[52px] gap-2 flex-shrink-0">
|
|
1253
|
-
<div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
|
|
1254
|
-
<button
|
|
1255
|
-
onClick={terminal.detachSession}
|
|
1256
|
-
className="text-sm text-[#8b949e] hover:text-[#e6edf3] active:text-[#22c55e] py-2 pr-2 -ml-2 min-h-[44px] flex items-center flex-shrink-0"
|
|
1257
|
-
>
|
|
1258
|
-
← <span className="hidden sm:inline ml-1">Workspaces</span>
|
|
1259
|
-
</button>
|
|
1260
|
-
<div className="text-sm text-[#8b949e] truncate">
|
|
1261
|
-
<span className="text-[#3fb950] shadow-glow">●</span>{" "}
|
|
1262
|
-
<span className="hidden sm:inline">{selectedMachine?.label || selectedMachine?.machineId}</span>
|
|
1263
|
-
{terminal.attachedSessionName && (
|
|
1264
|
-
<span className="text-[#e6edf3]">
|
|
1265
|
-
<span className="hidden sm:inline text-[#6e7681] mx-1">/</span>
|
|
1266
|
-
{terminal.attachedSessionName.split(':').pop()}
|
|
1267
|
-
</span>
|
|
1268
|
-
)}
|
|
1269
|
-
</div>
|
|
1270
|
-
</div>
|
|
1271
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
1272
|
-
{/* Input mode toggle for mobile */}
|
|
1273
|
-
{showMobileControls && (
|
|
1274
|
-
<button
|
|
1275
|
-
onClick={toggleInputMode}
|
|
1276
|
-
className={`px-3 py-2 text-sm rounded min-h-[44px] transition-all ${
|
|
1277
|
-
inputMode
|
|
1278
|
-
? 'bg-[#22c55e] text-[#0d1117] shadow-glow font-medium'
|
|
1279
|
-
: 'bg-[#21262d] text-[#e6edf3] hover:bg-[#30363d]'
|
|
1280
|
-
}`}
|
|
1281
|
-
>
|
|
1282
|
-
Input
|
|
1283
|
-
</button>
|
|
1284
|
-
)}
|
|
1285
|
-
<span className="text-xs text-[#6e7681] hidden sm:inline">Ctrl+Esc</span>
|
|
1286
|
-
<button
|
|
1287
|
-
onClick={terminal.detachSession}
|
|
1288
|
-
className="px-3 py-2 text-sm bg-[#21262d] hover:bg-[#30363d] active:bg-[#161b22] rounded text-[#e6edf3] min-h-[44px] border border-[#30363d]"
|
|
1289
|
-
>
|
|
1290
|
-
Detach
|
|
1291
|
-
</button>
|
|
1292
|
-
</div>
|
|
1293
|
-
</div>
|
|
1294
|
-
<div className={getTerminalContainerClass()}>
|
|
1295
|
-
<SessionTerminal
|
|
1296
|
-
ref={terminalRef}
|
|
1297
|
-
onData={handleKeyboardData}
|
|
1298
|
-
setWriteCallback={terminal.setWriteCallback}
|
|
1299
|
-
onResize={terminal.resize}
|
|
1300
|
-
allowTapFocus={inputMode || !showMobileControls}
|
|
1301
|
-
allowTouchScroll={!inputMode}
|
|
1302
|
-
onActivity={handleTerminalActivity}
|
|
1303
|
-
readOnly={isViewOnlySession}
|
|
1304
|
-
/>
|
|
1305
|
-
</div>
|
|
1306
|
-
{/* Mobile controls toolbar - show in input mode */}
|
|
1307
|
-
{showMobileControls && inputMode && (
|
|
1308
|
-
<TerminalControls
|
|
1309
|
-
onSendData={handleSendData}
|
|
1310
|
-
onFocusTerminal={handleFocusTerminal}
|
|
1311
|
-
keyboardVisible={keyboardVisible}
|
|
1312
|
-
modifiers={modifiers}
|
|
1313
|
-
onModifiersChange={setModifiers}
|
|
1314
|
-
/>
|
|
1315
|
-
)}
|
|
1316
|
-
{/* Floating controls - show when keyboard is hidden on mobile */}
|
|
1317
|
-
{showFloatingControls && (
|
|
1318
|
-
<FloatingControls
|
|
1319
|
-
onSendData={handleSendData}
|
|
1320
|
-
showJogWheel={inputMode}
|
|
1321
|
-
/>
|
|
1322
|
-
)}
|
|
1323
|
-
</div>
|
|
1324
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1325
|
-
</>
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// ========== Terminal Connecting View ==========
|
|
1330
|
-
if (view === "terminal") {
|
|
1331
|
-
const statusMessage = {
|
|
1332
|
-
disconnected: "Disconnected",
|
|
1333
|
-
connecting: "Connecting to relay...",
|
|
1334
|
-
connected: "Connected, authenticating...",
|
|
1335
|
-
handshaking: "Establishing secure connection...",
|
|
1336
|
-
established: "Connected!",
|
|
1337
|
-
error: "Connection failed",
|
|
1338
|
-
}[terminal.status];
|
|
1339
|
-
|
|
1340
|
-
return (
|
|
1341
|
-
<>
|
|
1342
|
-
<div className="h-screen w-screen flex flex-col items-center justify-center bg-[#0d1117] px-4">
|
|
1343
|
-
<div className="text-center">
|
|
1344
|
-
<div className="text-lg text-[#e6edf3] mb-2 break-words">
|
|
1345
|
-
Connecting to {selectedMachine?.label || selectedMachine?.machineId}
|
|
1346
|
-
</div>
|
|
1347
|
-
<div className="text-sm text-[#8b949e]">{statusMessage}</div>
|
|
1348
|
-
{terminal.status === "error" && (
|
|
1349
|
-
<button
|
|
1350
|
-
onClick={handleBackToMachines}
|
|
1351
|
-
className="mt-4 px-6 py-3 text-base bg-[#21262d] hover:bg-[#30363d] active:bg-[#161b22] rounded-lg text-[#e6edf3] min-h-[48px] border border-[#30363d]"
|
|
1352
|
-
>
|
|
1353
|
-
Back to Machines
|
|
1354
|
-
</button>
|
|
1355
|
-
)}
|
|
1356
|
-
</div>
|
|
1357
|
-
</div>
|
|
1358
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1359
|
-
</>
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// ========== Machine List View ==========
|
|
1364
|
-
// This is now the main/default view - shows machines and your identity
|
|
1365
|
-
return (
|
|
1366
|
-
<>
|
|
1367
|
-
<div className="h-screen w-screen flex flex-col bg-[#0d1117]">
|
|
1368
|
-
{/* Header with identity info */}
|
|
1369
|
-
<div className="bg-[#161b22] px-4 py-3 border-b border-[#30363d]">
|
|
1370
|
-
<div className="max-w-2xl mx-auto">
|
|
1371
|
-
{/* Connection status */}
|
|
1372
|
-
<div className="flex items-center justify-between mb-3">
|
|
1373
|
-
<div className="flex items-center gap-2">
|
|
1374
|
-
<span className={`w-2 h-2 rounded-full ${
|
|
1375
|
-
relay.status === "connected" ? "bg-[#3fb950] shadow-glow" :
|
|
1376
|
-
relay.status === "connecting" ? "bg-[#d29922] animate-pulse" :
|
|
1377
|
-
"bg-[#f85149]"
|
|
1378
|
-
}`} />
|
|
1379
|
-
<span className="text-sm text-[#8b949e]">
|
|
1380
|
-
{relay.status === "connected" ? "Connected" :
|
|
1381
|
-
relay.status === "connecting" ? "Connecting..." :
|
|
1382
|
-
"Disconnected"}
|
|
1383
|
-
</span>
|
|
1384
|
-
</div>
|
|
1385
|
-
<button
|
|
1386
|
-
onClick={relay.refreshMachines}
|
|
1387
|
-
className="text-xs text-[#6e7681] hover:text-[#e6edf3] px-2 py-1"
|
|
1388
|
-
>
|
|
1389
|
-
Refresh
|
|
1390
|
-
</button>
|
|
1391
|
-
</div>
|
|
1392
|
-
|
|
1393
|
-
{/* Your identity - prominent display */}
|
|
1394
|
-
{relay.publicKey && (
|
|
1395
|
-
<div className="bg-[#0d1117] rounded-lg p-3 border border-[#30363d]">
|
|
1396
|
-
<div className="flex items-center justify-between mb-2">
|
|
1397
|
-
<span className="text-xs text-[#8b949e]">Your Browser Identity</span>
|
|
1398
|
-
</div>
|
|
1399
|
-
<code className="block text-xs text-[#3fb950] break-all font-mono leading-relaxed mb-3">
|
|
1400
|
-
{relay.publicKey}
|
|
1401
|
-
</code>
|
|
1402
|
-
<p className="text-xs text-[#6e7681] mb-2">
|
|
1403
|
-
To get access, have the machine owner run:
|
|
1404
|
-
</p>
|
|
1405
|
-
<div className="flex items-center gap-2">
|
|
1406
|
-
<code className="flex-1 text-xs text-[#e6edf3] bg-[#161b22] px-2 py-2 rounded font-mono overflow-x-auto border border-[#30363d]">
|
|
1407
|
-
gssh access add "{relay.publicKey.slice(0, 20)}..."
|
|
1408
|
-
</code>
|
|
1409
|
-
<button
|
|
1410
|
-
onClick={copyAccessCommand}
|
|
1411
|
-
className="text-xs text-[#22c55e] hover:text-[#3fb950] bg-[#161b22] border border-[#30363d] px-3 py-2 rounded whitespace-nowrap hover:border-[#22c55e] transition-colors"
|
|
1412
|
-
>
|
|
1413
|
-
{copied ? "Copied!" : "Copy Command"}
|
|
1414
|
-
</button>
|
|
1415
|
-
</div>
|
|
1416
|
-
</div>
|
|
1417
|
-
)}
|
|
1418
|
-
</div>
|
|
1419
|
-
</div>
|
|
1420
|
-
|
|
1421
|
-
{/* Machine list */}
|
|
1422
|
-
<div className="flex-1 overflow-auto">
|
|
1423
|
-
{relay.status === "connecting" ? (
|
|
1424
|
-
<div className="flex items-center justify-center h-full">
|
|
1425
|
-
<div className="text-[#8b949e]">Connecting to relay...</div>
|
|
1426
|
-
</div>
|
|
1427
|
-
) : relay.machines.length === 0 ? (
|
|
1428
|
-
<div className="flex items-center justify-center h-full p-4">
|
|
1429
|
-
<div className="text-center max-w-md">
|
|
1430
|
-
<div className="text-[#8b949e] mb-2">No machines available</div>
|
|
1431
|
-
<p className="text-sm text-[#6e7681]">
|
|
1432
|
-
{relay.status === "connected"
|
|
1433
|
-
? "The machine may not be online. Check if 'gssh serve' is running."
|
|
1434
|
-
: "Unable to connect to relay."}
|
|
1435
|
-
</p>
|
|
1436
|
-
</div>
|
|
1437
|
-
</div>
|
|
1438
|
-
) : (
|
|
1439
|
-
<MachineListWeb {...machineListProps} />
|
|
1440
|
-
)}
|
|
1441
|
-
</div>
|
|
1442
|
-
|
|
1443
|
-
{/* Footer */}
|
|
1444
|
-
<div className="bg-[#161b22] px-4 py-2 border-t border-[#30363d]">
|
|
1445
|
-
<p className="text-xs text-[#6e7681] text-center">
|
|
1446
|
-
End-to-end encrypted via X3DH
|
|
1447
|
-
</p>
|
|
1448
|
-
</div>
|
|
1449
|
-
</div>
|
|
1450
|
-
<FlowWeb flow={flow} />
|
|
1451
|
-
<Toaster theme="dark" position="top-right" richColors />
|
|
1452
|
-
</>
|
|
1453
|
-
);
|
|
1454
|
-
}
|