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.tui.tsx
DELETED
|
@@ -1,2929 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI Application v2 - Using Shared Components
|
|
3
|
-
*
|
|
4
|
-
* Clean implementation using shared hooks and components:
|
|
5
|
-
* - useFlow for modal system
|
|
6
|
-
* - useMachineList for machine selection
|
|
7
|
-
* - useSpacesBrowser for workspace browsing
|
|
8
|
-
* - useProjectList for project selection
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createCliRenderer } from '@opentui/core';
|
|
12
|
-
import type { PasteEvent } from '@opentui/core';
|
|
13
|
-
import { createRoot, useKeyboard, useRenderer } from '@opentui/react';
|
|
14
|
-
import { useState, useEffect, useCallback, useReducer, Fragment, useRef } from 'react';
|
|
15
|
-
import { Toaster } from '@opentui-ui/toast/react';
|
|
16
|
-
|
|
17
|
-
// Terminal components
|
|
18
|
-
import { SessionTerminal } from './components/SessionTerminal.tui.js';
|
|
19
|
-
import { RemoteMachineScreen } from './components/RemoteMachineScreen.tui.js';
|
|
20
|
-
import { ScriptTerminal, type ScriptTerminalHandle } from './components/ScriptTerminal.tui.js';
|
|
21
|
-
import { ProjectOnboardingStepTUI } from './components/ProjectOnboardingStep.tui.js';
|
|
22
|
-
|
|
23
|
-
// Shared components and hooks
|
|
24
|
-
import {
|
|
25
|
-
useFlow,
|
|
26
|
-
useMachineList,
|
|
27
|
-
useSpacesBrowser,
|
|
28
|
-
useProjectList,
|
|
29
|
-
getDefaultShortcuts,
|
|
30
|
-
isFlowInput,
|
|
31
|
-
isFlowConfirmTyped,
|
|
32
|
-
isFlowWizard,
|
|
33
|
-
type MachineInfo,
|
|
34
|
-
type ProjectInfo,
|
|
35
|
-
} from './components/index.js';
|
|
36
|
-
import { FlowTUI } from './components/Flow.tui.js';
|
|
37
|
-
import { MachineListTUI } from './components/MachineList.tui.js';
|
|
38
|
-
import { SpacesBrowserTUI } from './components/SpacesBrowser.tui.js';
|
|
39
|
-
import type { TreeItem } from './components/SpacesBrowser.js';
|
|
40
|
-
import { ProjectListTUI } from './components/ProjectList.tui.js';
|
|
41
|
-
import { InboxTUI } from './components/Inbox.tui.js';
|
|
42
|
-
import { useInbox } from './components/Inbox.js';
|
|
43
|
-
import { EventsTui } from './components/Events.tui.js';
|
|
44
|
-
import { useEvents, toWideEventItem, type WideEventItem } from './components/Events.js';
|
|
45
|
-
import type { SavedEventFilter, WideEventFilter } from './types/events.js';
|
|
46
|
-
import { toast } from '@opentui-ui/toast';
|
|
47
|
-
import {
|
|
48
|
-
useNotifications,
|
|
49
|
-
type ToastNotification,
|
|
50
|
-
DEFAULT_NOTIFICATION_CONFIG,
|
|
51
|
-
getSessionLabel,
|
|
52
|
-
} from './notifications/index.js';
|
|
53
|
-
|
|
54
|
-
// Local state and config
|
|
55
|
-
import { useDaemonStatus, formatUptime, formatRelayStatus } from './hooks/useDaemonStatus.tui.js';
|
|
56
|
-
import {
|
|
57
|
-
setCurrentProject,
|
|
58
|
-
readProjectConfig,
|
|
59
|
-
getProjectBaseDir,
|
|
60
|
-
getProjectWorkspacesDir,
|
|
61
|
-
createProject,
|
|
62
|
-
projectExists,
|
|
63
|
-
} from './core/config.js';
|
|
64
|
-
import { localPreferencesService } from './core/preferences-service.js';
|
|
65
|
-
import type { NotificationConfig, NotificationTypeConfig } from './types/config.js';
|
|
66
|
-
|
|
67
|
-
// Git and workspace operations
|
|
68
|
-
import { listRemoteBranches, createWorktree, getDefaultBranch } from './core/git.js';
|
|
69
|
-
import { deleteProjectCore } from './core/workspace.js';
|
|
70
|
-
import { fetchUnstartedIssues, getLinearConfig } from './core/linear.js';
|
|
71
|
-
import { generateMarkdown } from './utils/markdown.js';
|
|
72
|
-
import { sanitizeForFileSystem, generateWorkspaceName, isValidBranchName, extractRepoName } from './utils/sanitize.js';
|
|
73
|
-
import { logger } from './utils/logger.js';
|
|
74
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
75
|
-
import { join } from 'path';
|
|
76
|
-
|
|
77
|
-
// Script execution
|
|
78
|
-
import type { LinearIssue } from './types/workspace.js';
|
|
79
|
-
|
|
80
|
-
// Project creation
|
|
81
|
-
import { listAllRepos, cloneRepository } from './core/github.js';
|
|
82
|
-
import { detectBundleInRepo, loadBundleFromPath } from './core/bundle.js';
|
|
83
|
-
import { applyProjectBundleState } from './core/project-lifecycle.js';
|
|
84
|
-
import { checkCommandExists } from './utils/deps.js';
|
|
85
|
-
import type { OnboardingStep } from './types/bundle.js';
|
|
86
|
-
|
|
87
|
-
// TUI hooks
|
|
88
|
-
import { useRemoteMachines, type RelayConfig } from './hooks/useRemoteMachines.tui.js';
|
|
89
|
-
import { useLocalSession } from './hooks/useLocalSession.tui.js';
|
|
90
|
-
import { useUserActivity } from './hooks/index.js';
|
|
91
|
-
import { useBundleRefreshAttachFlow } from './session/index.js';
|
|
92
|
-
import { useAttachController } from './app/session/useAttachController.js';
|
|
93
|
-
import { useProcessActions } from './app/session/useProcessActions.js';
|
|
94
|
-
import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
|
|
95
|
-
import { buildEditProcessesCommand } from './lib/processes/editor.js';
|
|
96
|
-
import { loadProcessesConfigWithDiagnostics } from './lib/processes/config.js';
|
|
97
|
-
import {
|
|
98
|
-
consumeLegacyCleanupReminderForTui,
|
|
99
|
-
initializeSecretRuntime,
|
|
100
|
-
} from './core/secret-runtime.js';
|
|
101
|
-
import {
|
|
102
|
-
resolveInboxCommand,
|
|
103
|
-
resolveMachineListCommand,
|
|
104
|
-
resolveSessionBrowserCommand,
|
|
105
|
-
} from './app/input/sessionCommands.js';
|
|
106
|
-
import { resolveLocalTerminalSyncAction, type AppView } from './tui/local-terminal-sync.js';
|
|
107
|
-
import {
|
|
108
|
-
getKeyboardInputChunk,
|
|
109
|
-
getNumericInputChunk,
|
|
110
|
-
normalizeInputText,
|
|
111
|
-
} from './tui/input-text.js';
|
|
112
|
-
|
|
113
|
-
// Types
|
|
114
|
-
import type { InboxItem } from './lib/tmux-lite/cli.js';
|
|
115
|
-
|
|
116
|
-
// ============================================================================
|
|
117
|
-
// Workspace Flow Types (Custom State Machine)
|
|
118
|
-
// ============================================================================
|
|
119
|
-
|
|
120
|
-
/** Available workspace creation sources */
|
|
121
|
-
type WorkspaceSource = 'branch' | 'linear' | 'manual';
|
|
122
|
-
|
|
123
|
-
/** Workspace flow states - explicit state machine */
|
|
124
|
-
type WorkspaceFlowState =
|
|
125
|
-
| { type: 'closed' }
|
|
126
|
-
| { type: 'source-select'; selectedIndex: number; options: Array<{ label: string; description: string; value: WorkspaceSource }> }
|
|
127
|
-
| { type: 'loading'; title: string; message: string }
|
|
128
|
-
| { type: 'branch-select'; branches: string[]; selectedIndex: number }
|
|
129
|
-
| { type: 'linear-select'; issues: LinearIssue[]; selectedIndex: number }
|
|
130
|
-
| { type: 'manual-name-input'; inputValue: string; error: string | null }
|
|
131
|
-
| { type: 'manual-branch-input'; workspaceName: string; inputValue: string; error: string | null }
|
|
132
|
-
| { type: 'creating'; workspaceName: string; message?: string };
|
|
133
|
-
|
|
134
|
-
/** Project flow states - explicit state machine for project creation */
|
|
135
|
-
type ProjectFlowState =
|
|
136
|
-
| { type: 'closed' }
|
|
137
|
-
| { type: 'loading-repos' }
|
|
138
|
-
| { type: 'repo-select'; repos: string[]; selectedIndex: number }
|
|
139
|
-
| { type: 'cloning'; repo: string }
|
|
140
|
-
| { type: 'onboarding';
|
|
141
|
-
repo: string;
|
|
142
|
-
projectName: string;
|
|
143
|
-
baseBranch: string;
|
|
144
|
-
bundleDir: string;
|
|
145
|
-
bundleName: string;
|
|
146
|
-
steps: OnboardingStep[];
|
|
147
|
-
currentStep: number;
|
|
148
|
-
collectedValues: Record<string, string>;
|
|
149
|
-
collectedSecrets: Record<string, string>;
|
|
150
|
-
inputValue: string;
|
|
151
|
-
confirmStatus?: 'checking' | 'found' | 'missing' | null;
|
|
152
|
-
}
|
|
153
|
-
| { type: 'creating'; projectName: string };
|
|
154
|
-
|
|
155
|
-
/** Settings flow states - explicit state machine for settings modal */
|
|
156
|
-
type SettingsFlowState =
|
|
157
|
-
| { type: 'closed' }
|
|
158
|
-
| { type: 'main-menu'; selectedIndex: number; config: NotificationConfig }
|
|
159
|
-
| { type: 'types-menu'; selectedIndex: number; config: NotificationConfig }
|
|
160
|
-
| { type: 'edit-duration'; value: string; config: NotificationConfig }
|
|
161
|
-
| { type: 'edit-hold-duration'; value: string; config: NotificationConfig };
|
|
162
|
-
|
|
163
|
-
// ============================================================================
|
|
164
|
-
// Constants
|
|
165
|
-
// ============================================================================
|
|
166
|
-
|
|
167
|
-
const COLORS = {
|
|
168
|
-
border: '#555555',
|
|
169
|
-
borderFocused: '#00AAFF',
|
|
170
|
-
text: '#FFFFFF',
|
|
171
|
-
textDim: '#888888',
|
|
172
|
-
selected: '#00AAFF',
|
|
173
|
-
title: '#00FF88',
|
|
174
|
-
statusBar: '#333333',
|
|
175
|
-
loading: '#FFAA00',
|
|
176
|
-
error: '#FF4444',
|
|
177
|
-
// ASCII art gradient
|
|
178
|
-
gradient1: '#00FFFF',
|
|
179
|
-
gradient2: '#00DDFF',
|
|
180
|
-
gradient3: '#00BBFF',
|
|
181
|
-
gradient4: '#0099FF',
|
|
182
|
-
gradient5: '#0077FF',
|
|
183
|
-
gradient6: '#0055FF',
|
|
184
|
-
asciiBox: '#444466',
|
|
185
|
-
subtitle: '#888899',
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// ASCII art header
|
|
189
|
-
const ASCII_LINES = [
|
|
190
|
-
{ text: '╔══════════════════════════════════════════════════════════════╗', color: COLORS.asciiBox },
|
|
191
|
-
{ text: '║ ║', color: COLORS.asciiBox },
|
|
192
|
-
{ text: '║ ███████╗██████╗ █████╗ ██████╗███████╗███████╗ ║', color: COLORS.gradient1 },
|
|
193
|
-
{ text: '║ ██╔════╝██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝ ║', color: COLORS.gradient2 },
|
|
194
|
-
{ text: '║ ███████╗██████╔╝███████║██║ █████╗ ███████╗ ║', color: COLORS.gradient3 },
|
|
195
|
-
{ text: '║ ╚════██║██╔═══╝ ██╔══██║██║ ██╔══╝ ╚════██║ ║', color: COLORS.gradient4 },
|
|
196
|
-
{ text: '║ ███████║██║ ██║ ██║╚██████╗███████╗███████║ ║', color: COLORS.gradient5 },
|
|
197
|
-
{ text: '║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝ ║', color: COLORS.gradient6 },
|
|
198
|
-
{ text: '║ ║', color: COLORS.asciiBox },
|
|
199
|
-
{ text: '║ worktree manager ║', color: COLORS.subtitle },
|
|
200
|
-
{ text: '║ ║', color: COLORS.asciiBox },
|
|
201
|
-
{ text: '╚══════════════════════════════════════════════════════════════╝', color: COLORS.asciiBox },
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
// ============================================================================
|
|
205
|
-
// App State
|
|
206
|
-
// ============================================================================
|
|
207
|
-
|
|
208
|
-
type PanelFocus = 'projects' | 'workspaces';
|
|
209
|
-
|
|
210
|
-
interface AppState {
|
|
211
|
-
view: AppView;
|
|
212
|
-
panelFocus: PanelFocus;
|
|
213
|
-
selectedMachine: MachineInfo | null;
|
|
214
|
-
isLoading: boolean;
|
|
215
|
-
error: string | null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
type AppAction =
|
|
219
|
-
| { type: 'SET_VIEW'; view: AppView }
|
|
220
|
-
| { type: 'SET_PANEL_FOCUS'; focus: PanelFocus }
|
|
221
|
-
| { type: 'SET_MACHINE'; machine: MachineInfo | null }
|
|
222
|
-
| { type: 'SET_LOADING'; loading: boolean }
|
|
223
|
-
| { type: 'SET_ERROR'; error: string | null }
|
|
224
|
-
| { type: 'SWITCH_PANEL' };
|
|
225
|
-
|
|
226
|
-
function appReducer(state: AppState, action: AppAction): AppState {
|
|
227
|
-
switch (action.type) {
|
|
228
|
-
case 'SET_VIEW':
|
|
229
|
-
return { ...state, view: action.view };
|
|
230
|
-
case 'SET_PANEL_FOCUS':
|
|
231
|
-
return { ...state, panelFocus: action.focus };
|
|
232
|
-
case 'SET_MACHINE':
|
|
233
|
-
return { ...state, selectedMachine: action.machine };
|
|
234
|
-
case 'SET_LOADING':
|
|
235
|
-
return { ...state, isLoading: action.loading };
|
|
236
|
-
case 'SET_ERROR':
|
|
237
|
-
return { ...state, error: action.error };
|
|
238
|
-
case 'SWITCH_PANEL':
|
|
239
|
-
return { ...state, panelFocus: state.panelFocus === 'projects' ? 'workspaces' : 'projects' };
|
|
240
|
-
default:
|
|
241
|
-
return state;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ============================================================================
|
|
246
|
-
// Props
|
|
247
|
-
// ============================================================================
|
|
248
|
-
|
|
249
|
-
export interface AppProps {
|
|
250
|
-
relayConfig?: RelayConfig;
|
|
251
|
-
onQuit?: () => void;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ============================================================================
|
|
255
|
-
// Main App Component
|
|
256
|
-
// ============================================================================
|
|
257
|
-
|
|
258
|
-
function App({ relayConfig, onQuit }: AppProps) {
|
|
259
|
-
const isRemoteMode = !!relayConfig;
|
|
260
|
-
|
|
261
|
-
// Force re-render counter for resize
|
|
262
|
-
const [, forceUpdate] = useState(0);
|
|
263
|
-
|
|
264
|
-
// Handle terminal resize
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
const handleResize = () => {
|
|
267
|
-
// Force React to re-render by updating state
|
|
268
|
-
forceUpdate(n => n + 1);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
process.on('SIGWINCH', handleResize);
|
|
272
|
-
return () => {
|
|
273
|
-
process.removeListener('SIGWINCH', handleResize);
|
|
274
|
-
};
|
|
275
|
-
}, []);
|
|
276
|
-
|
|
277
|
-
// App state
|
|
278
|
-
const [state, dispatch] = useReducer(appReducer, {
|
|
279
|
-
view: isRemoteMode ? 'machines' : 'projects',
|
|
280
|
-
panelFocus: 'projects',
|
|
281
|
-
selectedMachine: null,
|
|
282
|
-
isLoading: true,
|
|
283
|
-
error: null,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Track when we're switching sessions (to prevent detach handler from navigating away)
|
|
287
|
-
const sessionSwitchingRef = useRef(false);
|
|
288
|
-
const scriptTerminalRef = useRef<ScriptTerminalHandle | null>(null);
|
|
289
|
-
const renderer = useRenderer();
|
|
290
|
-
|
|
291
|
-
// Shared Flow hook (for non-workspace flows)
|
|
292
|
-
const flow = useFlow({
|
|
293
|
-
onError: (error) => dispatch({ type: 'SET_ERROR', error: error.message }),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Workspace creation flow (custom state machine)
|
|
297
|
-
const [workspaceFlow, setWorkspaceFlow] = useState<WorkspaceFlowState>({ type: 'closed' });
|
|
298
|
-
const [scriptWorkspaceName, setScriptWorkspaceName] = useState<string>('workspace');
|
|
299
|
-
|
|
300
|
-
// Project creation flow (custom state machine)
|
|
301
|
-
const [projectFlow, setProjectFlow] = useState<ProjectFlowState>({ type: 'closed' });
|
|
302
|
-
|
|
303
|
-
// Settings flow (custom state machine)
|
|
304
|
-
const [settingsFlow, setSettingsFlow] = useState<SettingsFlowState>({ type: 'closed' });
|
|
305
|
-
const [notificationConfig, setNotificationConfig] = useState<NotificationConfig>(DEFAULT_NOTIFICATION_CONFIG);
|
|
306
|
-
|
|
307
|
-
// Events view state
|
|
308
|
-
const [eventsWorkspaceId, setEventsWorkspaceId] = useState<string | null>(null);
|
|
309
|
-
|
|
310
|
-
// View-only session state (true when attached to a running process session)
|
|
311
|
-
const [isViewOnlySession, setIsViewOnlySession] = useState(false);
|
|
312
|
-
const [pendingProcessEditWorkspaceId, setPendingProcessEditWorkspaceId] = useState<string | null>(null);
|
|
313
|
-
const pendingProcessEditWorkspacesRef = useRef<unknown[] | null>(null);
|
|
314
|
-
|
|
315
|
-
// Remote machines hook
|
|
316
|
-
const remoteMachines = useRemoteMachines({
|
|
317
|
-
relayConfig,
|
|
318
|
-
onError: (error) => dispatch({ type: 'SET_ERROR', error: error.message }),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const isLocalMachineContext = !isRemoteMode || state.selectedMachine?.machineId === 'local';
|
|
322
|
-
|
|
323
|
-
// Local machine session engine hook
|
|
324
|
-
const localSession = useLocalSession({ enabled: isLocalMachineContext });
|
|
325
|
-
const {
|
|
326
|
-
status: localSessionStatus,
|
|
327
|
-
mode: localSessionMode,
|
|
328
|
-
requestProjects: requestLocalProjects,
|
|
329
|
-
requestWorkspaces: requestLocalWorkspaces,
|
|
330
|
-
requestSessions: requestLocalSessions,
|
|
331
|
-
requestInbox: requestLocalInbox,
|
|
332
|
-
clearInbox: clearLocalInbox,
|
|
333
|
-
markInboxRead: markLocalInboxRead,
|
|
334
|
-
attachSession: attachLocalSession,
|
|
335
|
-
detachSession: detachLocalSession,
|
|
336
|
-
killSession: killLocalSession,
|
|
337
|
-
deleteWorkspace: deleteLocalWorkspace,
|
|
338
|
-
send: sendLocalPty,
|
|
339
|
-
resize: resizeLocalPty,
|
|
340
|
-
setWriteCallback: setLocalWriteCallback,
|
|
341
|
-
projects: localProjects,
|
|
342
|
-
workspaces: localWorkspaces,
|
|
343
|
-
sessions: localSessions,
|
|
344
|
-
inbox: localInbox,
|
|
345
|
-
inboxUnreadCount: localInboxUnreadCount,
|
|
346
|
-
attachedSessionId: localAttachedSessionId,
|
|
347
|
-
attachedSessionName: localAttachedSessionName,
|
|
348
|
-
scriptState: localScriptState,
|
|
349
|
-
commandError: localCommandError,
|
|
350
|
-
getBundleRefreshPlan: getLocalBundleRefreshPlan,
|
|
351
|
-
applyBundleRefresh: applyLocalBundleRefresh,
|
|
352
|
-
startProcess: startLocalProcess,
|
|
353
|
-
stopProcess: stopLocalProcess,
|
|
354
|
-
requestEvents: requestLocalEvents,
|
|
355
|
-
events: localEvents,
|
|
356
|
-
liveEventIds: localLiveEventIds,
|
|
357
|
-
savedEventFilters: localSavedEventFilters,
|
|
358
|
-
} = localSession;
|
|
359
|
-
|
|
360
|
-
const currentProject =
|
|
361
|
-
localProjects.find((project) => project.isCurrent)?.name ?? localProjects[0]?.name ?? null;
|
|
362
|
-
|
|
363
|
-
const getLocalAttachSize = useCallback(() => {
|
|
364
|
-
let cols = process.stdout.columns || 0;
|
|
365
|
-
let rows = process.stdout.rows || 0;
|
|
366
|
-
if (cols <= 0 || rows <= 0) {
|
|
367
|
-
const size = (process.stdout as { getWindowSize?: () => number[] }).getWindowSize?.();
|
|
368
|
-
if (Array.isArray(size) && size.length >= 2) {
|
|
369
|
-
cols = size[0];
|
|
370
|
-
rows = size[1];
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
cols: cols > 0 ? cols : 80,
|
|
376
|
-
rows: Math.max(1, (rows > 0 ? rows : 24) - 1),
|
|
377
|
-
};
|
|
378
|
-
}, []);
|
|
379
|
-
|
|
380
|
-
const bundleRefreshAttach = useBundleRefreshAttachFlow({
|
|
381
|
-
flow,
|
|
382
|
-
commandError: localCommandError,
|
|
383
|
-
attachSession: (params) => attachLocalSession(params),
|
|
384
|
-
getBundleRefreshPlan: getLocalBundleRefreshPlan,
|
|
385
|
-
applyBundleRefresh: applyLocalBundleRefresh,
|
|
386
|
-
resolveProjectName: (workspaceId) => {
|
|
387
|
-
const separator = workspaceId.indexOf(':');
|
|
388
|
-
if (separator > 0) {
|
|
389
|
-
return workspaceId.slice(0, separator);
|
|
390
|
-
}
|
|
391
|
-
return currentProject;
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const {
|
|
396
|
-
attach: attachLocal,
|
|
397
|
-
attachFromSelection: attachLocalFromSelection,
|
|
398
|
-
} = useAttachController({
|
|
399
|
-
flow,
|
|
400
|
-
attachSessionWithBundleRefresh: bundleRefreshAttach.attachSessionWithBundleRefresh,
|
|
401
|
-
defaultProjectName: currentProject,
|
|
402
|
-
getAttachSize: getLocalAttachSize,
|
|
403
|
-
resolveProjectName: (workspaceId) => {
|
|
404
|
-
const separator = workspaceId.indexOf(':');
|
|
405
|
-
if (separator > 0) {
|
|
406
|
-
return workspaceId.slice(0, separator);
|
|
407
|
-
}
|
|
408
|
-
return currentProject;
|
|
409
|
-
},
|
|
410
|
-
preflightSessionAttach: async (sessionId) => {
|
|
411
|
-
const sessionInfo = localSessions.find((session) => session.id === sessionId);
|
|
412
|
-
if (!sessionInfo) {
|
|
413
|
-
await refreshWorkspaces();
|
|
414
|
-
dispatch({ type: 'SET_ERROR', error: 'Session no longer exists. The session list has been refreshed.' });
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!sessionInfo.attached) {
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return new Promise<boolean>((resolve) => {
|
|
423
|
-
flow.showConfirm({
|
|
424
|
-
title: 'Session In Use',
|
|
425
|
-
message: 'This session is currently attached. Steal it?',
|
|
426
|
-
variant: 'warning',
|
|
427
|
-
confirmLabel: 'Steal',
|
|
428
|
-
onConfirm: () => {
|
|
429
|
-
resolve(true);
|
|
430
|
-
},
|
|
431
|
-
onCancel: () => {
|
|
432
|
-
resolve(false);
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
},
|
|
437
|
-
onBeforeAttach: ({ target, params }) => {
|
|
438
|
-
sessionSwitchingRef.current = true;
|
|
439
|
-
|
|
440
|
-
if (target === 'workspace' && params.workspaceId && !params.command) {
|
|
441
|
-
setScriptWorkspaceName(params.workspaceId.split(':').slice(-1)[0] ?? params.workspaceId);
|
|
442
|
-
dispatch({ type: 'SET_VIEW', view: 'scripts' });
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
onAttachSuccess: () => {
|
|
446
|
-
dispatch({ type: 'SET_VIEW', view: 'terminal' });
|
|
447
|
-
},
|
|
448
|
-
onAttachCancelled: () => {
|
|
449
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
450
|
-
},
|
|
451
|
-
onAttachError: ({ target, message }) => {
|
|
452
|
-
const isWorkspaceScriptFailure = message.startsWith('Workspace scripts failed during');
|
|
453
|
-
const showScriptView = target === 'workspace';
|
|
454
|
-
|
|
455
|
-
if (!isWorkspaceScriptFailure || !showScriptView) {
|
|
456
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
flow.showMessage({
|
|
460
|
-
title: isWorkspaceScriptFailure ? 'Workspace Script Failed' : 'Session Failed',
|
|
461
|
-
message,
|
|
462
|
-
variant: 'error',
|
|
463
|
-
});
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
468
|
-
flow,
|
|
469
|
-
deleteWorkspace: deleteLocalWorkspace,
|
|
470
|
-
onBeforeDelete: ({ target }) => {
|
|
471
|
-
setScriptWorkspaceName(target.workspaceName);
|
|
472
|
-
dispatch({ type: 'SET_VIEW', view: 'scripts' });
|
|
473
|
-
},
|
|
474
|
-
onDeleteSuccess: async () => {
|
|
475
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
476
|
-
await refreshWorkspaces();
|
|
477
|
-
},
|
|
478
|
-
onDeleteError: async ({ message }) => {
|
|
479
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
480
|
-
flow.showMessage({
|
|
481
|
-
title: 'Delete Failed',
|
|
482
|
-
message,
|
|
483
|
-
variant: 'error',
|
|
484
|
-
});
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// Daemon status hook (tmux-lite and serve)
|
|
489
|
-
const { status: daemonStatus } = useDaemonStatus({ pollInterval: 5000 });
|
|
490
|
-
|
|
491
|
-
// Load persisted notification preferences.
|
|
492
|
-
useEffect(() => {
|
|
493
|
-
let mounted = true;
|
|
494
|
-
void localPreferencesService.getNotificationConfig().then((config) => {
|
|
495
|
-
if (mounted) {
|
|
496
|
-
setNotificationConfig(config);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
return () => {
|
|
501
|
-
mounted = false;
|
|
502
|
-
};
|
|
503
|
-
}, []);
|
|
504
|
-
|
|
505
|
-
// ========== Data Loading ==========
|
|
506
|
-
|
|
507
|
-
// Load projects
|
|
508
|
-
const refreshProjects = useCallback(async () => {
|
|
509
|
-
if (!isLocalMachineContext) {
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
await requestLocalProjects();
|
|
513
|
-
}, [isLocalMachineContext, requestLocalProjects]);
|
|
514
|
-
|
|
515
|
-
// Load workspaces for current project
|
|
516
|
-
const refreshWorkspaces = useCallback(async () => {
|
|
517
|
-
if (!isLocalMachineContext) {
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
await Promise.all([
|
|
521
|
-
requestLocalWorkspaces(),
|
|
522
|
-
requestLocalSessions(),
|
|
523
|
-
]);
|
|
524
|
-
}, [isLocalMachineContext, requestLocalSessions, requestLocalWorkspaces]);
|
|
525
|
-
|
|
526
|
-
// Load inbox
|
|
527
|
-
const refreshInbox = useCallback(async () => {
|
|
528
|
-
if (!isLocalMachineContext) {
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
await requestLocalInbox();
|
|
532
|
-
}, [isLocalMachineContext, requestLocalInbox]);
|
|
533
|
-
|
|
534
|
-
// Initial load
|
|
535
|
-
useEffect(() => {
|
|
536
|
-
const load = async () => {
|
|
537
|
-
dispatch({ type: 'SET_LOADING', loading: true });
|
|
538
|
-
try {
|
|
539
|
-
await refreshProjects();
|
|
540
|
-
// Load inbox in background (don't block initial render)
|
|
541
|
-
refreshInbox().catch((error) => {
|
|
542
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
543
|
-
logger.error(`[tui] Background inbox refresh failed: ${detail}`);
|
|
544
|
-
});
|
|
545
|
-
} catch (err) {
|
|
546
|
-
dispatch({ type: 'SET_ERROR', error: err instanceof Error ? err.message : 'Failed to load' });
|
|
547
|
-
} finally {
|
|
548
|
-
dispatch({ type: 'SET_LOADING', loading: false });
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
load();
|
|
552
|
-
}, []);
|
|
553
|
-
|
|
554
|
-
// Load workspaces when project changes
|
|
555
|
-
useEffect(() => {
|
|
556
|
-
if (currentProject) {
|
|
557
|
-
refreshWorkspaces();
|
|
558
|
-
}
|
|
559
|
-
}, [currentProject, refreshWorkspaces]);
|
|
560
|
-
|
|
561
|
-
// ========== Action Handlers ==========
|
|
562
|
-
|
|
563
|
-
// Select a project
|
|
564
|
-
const handleSelectProject = useCallback((project: ProjectInfo) => {
|
|
565
|
-
setCurrentProject(project.name);
|
|
566
|
-
void requestLocalProjects().catch((error) => {
|
|
567
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
568
|
-
console.error('[tui] Failed to refresh projects after project select:', message);
|
|
569
|
-
});
|
|
570
|
-
void requestLocalWorkspaces().catch((error) => {
|
|
571
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
572
|
-
console.error('[tui] Failed to refresh workspaces after project select:', message);
|
|
573
|
-
});
|
|
574
|
-
void requestLocalSessions().catch((error) => {
|
|
575
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
576
|
-
console.error('[tui] Failed to refresh sessions after project select:', message);
|
|
577
|
-
});
|
|
578
|
-
dispatch({ type: 'SET_PANEL_FOCUS', focus: 'workspaces' });
|
|
579
|
-
}, [requestLocalProjects, requestLocalSessions, requestLocalWorkspaces]);
|
|
580
|
-
|
|
581
|
-
// Delete project - show typed confirmation
|
|
582
|
-
const handleDeleteProject = useCallback((project: ProjectInfo) => {
|
|
583
|
-
flow.showConfirmTyped({
|
|
584
|
-
title: 'Delete Project',
|
|
585
|
-
message: `Are you sure you want to delete project "${project.name}"?`,
|
|
586
|
-
confirmText: project.name,
|
|
587
|
-
warning: 'This will delete all workspaces in this project!',
|
|
588
|
-
onConfirm: async () => {
|
|
589
|
-
flow.showLoading({ title: 'Deleting', message: 'Preparing...' });
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
const result = await deleteProjectCore(project.name, {
|
|
593
|
-
nonInteractive: true, // TUI is non-interactive for scripts
|
|
594
|
-
onProgress: (message) => {
|
|
595
|
-
flow.showLoading({ title: 'Deleting', message });
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
if (!result.success && result.errors.length > 0) {
|
|
600
|
-
console.error('[tui] Project deletion errors:', result.errors);
|
|
601
|
-
flow.close();
|
|
602
|
-
flow.showMessage({
|
|
603
|
-
title: 'Delete Failed',
|
|
604
|
-
message: `Failed to delete project "${project.name}". Check logs for details.`,
|
|
605
|
-
variant: 'error',
|
|
606
|
-
});
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
} catch (error) {
|
|
610
|
-
console.error('[tui] Failed to delete project:', error);
|
|
611
|
-
flow.close();
|
|
612
|
-
flow.showMessage({
|
|
613
|
-
title: 'Delete Failed',
|
|
614
|
-
message: `An unexpected error occurred while deleting project "${project.name}".`,
|
|
615
|
-
variant: 'error',
|
|
616
|
-
});
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
flow.close();
|
|
621
|
-
await refreshProjects();
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
}, [flow, refreshProjects]);
|
|
625
|
-
|
|
626
|
-
// Attach to session using embedded terminal
|
|
627
|
-
const handleAttachSession = useCallback(async (params: { sessionId?: string; workspaceId?: string; viewOnly?: boolean }) => {
|
|
628
|
-
setIsViewOnlySession(params.viewOnly ?? false);
|
|
629
|
-
await attachLocalFromSelection(params);
|
|
630
|
-
}, [attachLocalFromSelection]);
|
|
631
|
-
|
|
632
|
-
// Open editor on .gitspace/processes.json in the workspace
|
|
633
|
-
const handleEditProcesses = useCallback(({ workspaceId }: { workspaceId: string }) => {
|
|
634
|
-
setIsViewOnlySession(false);
|
|
635
|
-
pendingProcessEditWorkspacesRef.current = localWorkspaces;
|
|
636
|
-
setPendingProcessEditWorkspaceId(workspaceId);
|
|
637
|
-
const commandSpec = buildEditProcessesCommand();
|
|
638
|
-
void attachLocal({
|
|
639
|
-
workspaceId,
|
|
640
|
-
command: commandSpec.command,
|
|
641
|
-
args: commandSpec.args,
|
|
642
|
-
}).then((attached) => {
|
|
643
|
-
if (!attached) {
|
|
644
|
-
pendingProcessEditWorkspacesRef.current = null;
|
|
645
|
-
setPendingProcessEditWorkspaceId(null);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
}, [attachLocal, localWorkspaces]);
|
|
649
|
-
|
|
650
|
-
useEffect(() => {
|
|
651
|
-
if (!pendingProcessEditWorkspaceId) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (state.view !== 'projects' || localSessionMode !== 'browsing') {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
pendingProcessEditWorkspacesRef.current &&
|
|
660
|
-
pendingProcessEditWorkspacesRef.current === localWorkspaces
|
|
661
|
-
) {
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
pendingProcessEditWorkspacesRef.current = null;
|
|
665
|
-
|
|
666
|
-
const workspace = localWorkspaces.find((item) => item.id === pendingProcessEditWorkspaceId);
|
|
667
|
-
if (!workspace) {
|
|
668
|
-
setPendingProcessEditWorkspaceId(null);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const diagnostics = loadProcessesConfigWithDiagnostics(workspace.path);
|
|
673
|
-
if (diagnostics.error) {
|
|
674
|
-
flow.showMessage({
|
|
675
|
-
title: 'Invalid Processes Config',
|
|
676
|
-
message: diagnostics.error,
|
|
677
|
-
variant: 'error',
|
|
678
|
-
});
|
|
679
|
-
} else {
|
|
680
|
-
const processCount = diagnostics.config.processes.length;
|
|
681
|
-
flow.showMessage({
|
|
682
|
-
title: 'Processes Config Updated',
|
|
683
|
-
message: processCount === 0
|
|
684
|
-
? 'Config is valid. No processes are defined yet.'
|
|
685
|
-
: `Config is valid. ${processCount} process${processCount === 1 ? '' : 'es'} defined.`,
|
|
686
|
-
variant: 'success',
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
setPendingProcessEditWorkspaceId(null);
|
|
691
|
-
void refreshWorkspaces();
|
|
692
|
-
}, [
|
|
693
|
-
flow,
|
|
694
|
-
localSessionMode,
|
|
695
|
-
localWorkspaces,
|
|
696
|
-
pendingProcessEditWorkspaceId,
|
|
697
|
-
refreshWorkspaces,
|
|
698
|
-
state.view,
|
|
699
|
-
]);
|
|
700
|
-
|
|
701
|
-
// Handle terminal detach
|
|
702
|
-
const handleTerminalDetach = useCallback(async () => {
|
|
703
|
-
// Don't navigate away if we're in the middle of switching sessions
|
|
704
|
-
if (sessionSwitchingRef.current) return;
|
|
705
|
-
|
|
706
|
-
setIsViewOnlySession(false);
|
|
707
|
-
await detachLocalSession();
|
|
708
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
709
|
-
await refreshWorkspaces();
|
|
710
|
-
}, [detachLocalSession, refreshWorkspaces]);
|
|
711
|
-
|
|
712
|
-
// Delete workspace
|
|
713
|
-
const handleDeleteWorkspace = useCallback((workspace: { id: string; name: string; sessionCount: number }) => {
|
|
714
|
-
flow.showConfirmTyped({
|
|
715
|
-
title: 'Delete Workspace',
|
|
716
|
-
message: `Are you sure you want to delete workspace "${workspace.name}"?`,
|
|
717
|
-
confirmText: workspace.name,
|
|
718
|
-
warning: workspace.sessionCount > 0 ? `This will kill ${workspace.sessionCount} active session(s)!` : undefined,
|
|
719
|
-
onConfirm: async () => {
|
|
720
|
-
if (!currentProject) return;
|
|
721
|
-
await deleteWorkspaceWithPrompt({
|
|
722
|
-
projectName: currentProject,
|
|
723
|
-
workspaceId: workspace.id,
|
|
724
|
-
workspaceName: workspace.name,
|
|
725
|
-
});
|
|
726
|
-
},
|
|
727
|
-
});
|
|
728
|
-
}, [currentProject, flow, deleteWorkspaceWithPrompt]);
|
|
729
|
-
|
|
730
|
-
// Delete session
|
|
731
|
-
const handleDeleteSession = useCallback((sessionId: string, sessionName: string) => {
|
|
732
|
-
flow.showConfirm({
|
|
733
|
-
title: 'Kill Session',
|
|
734
|
-
message: `Kill session "${sessionName}"?`,
|
|
735
|
-
variant: 'warning',
|
|
736
|
-
confirmLabel: 'Kill',
|
|
737
|
-
onConfirm: async () => {
|
|
738
|
-
try {
|
|
739
|
-
await killLocalSession(sessionId);
|
|
740
|
-
} catch (err) {
|
|
741
|
-
dispatch({ type: 'SET_ERROR', error: err instanceof Error ? err.message : 'Failed to kill session' });
|
|
742
|
-
}
|
|
743
|
-
},
|
|
744
|
-
});
|
|
745
|
-
}, [flow, killLocalSession]);
|
|
746
|
-
|
|
747
|
-
// ========== Workspace Creation (Custom State Machine) ==========
|
|
748
|
-
|
|
749
|
-
// Core function to create workspace and open session
|
|
750
|
-
const createWorkspaceAndOpenSession = useCallback(async (
|
|
751
|
-
workspaceName: string,
|
|
752
|
-
branchName: string,
|
|
753
|
-
existsRemotely: boolean,
|
|
754
|
-
linearIssue?: LinearIssue
|
|
755
|
-
) => {
|
|
756
|
-
const projectName = currentProject;
|
|
757
|
-
if (!projectName) return;
|
|
758
|
-
|
|
759
|
-
try {
|
|
760
|
-
const baseDir = getProjectBaseDir(projectName);
|
|
761
|
-
const workspacesDir = getProjectWorkspacesDir(projectName);
|
|
762
|
-
const workspacePath = join(workspacesDir, workspaceName);
|
|
763
|
-
const config = readProjectConfig(projectName);
|
|
764
|
-
|
|
765
|
-
// Check if workspace already exists
|
|
766
|
-
if (existsSync(workspacePath)) {
|
|
767
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
768
|
-
dispatch({ type: 'SET_ERROR', error: `Workspace "${workspaceName}" already exists` });
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
setWorkspaceFlow({ type: 'creating', workspaceName });
|
|
773
|
-
|
|
774
|
-
// Create worktree
|
|
775
|
-
await createWorktree(baseDir, workspacePath, branchName, config.baseBranch, {
|
|
776
|
-
existsRemotely,
|
|
777
|
-
onProgress: (message) => {
|
|
778
|
-
setWorkspaceFlow({ type: 'creating', workspaceName, message });
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
// Save Linear issue if present
|
|
783
|
-
if (linearIssue) {
|
|
784
|
-
const linearConfig = await getLinearConfig(projectName);
|
|
785
|
-
if (linearConfig.apiKey) {
|
|
786
|
-
const promptDir = join(workspacePath, '.prompt');
|
|
787
|
-
mkdirSync(promptDir, { recursive: true });
|
|
788
|
-
const markdown = await generateMarkdown(linearIssue, promptDir, linearConfig.apiKey);
|
|
789
|
-
writeFileSync(join(promptDir, 'issue.md'), markdown, 'utf-8');
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
794
|
-
await refreshWorkspaces();
|
|
795
|
-
|
|
796
|
-
// Attach through local backend (runs pre/setup/select scripts + creates session).
|
|
797
|
-
const workspaceId = `${projectName}:${workspaceName}`;
|
|
798
|
-
const attached = await attachLocal({
|
|
799
|
-
workspaceId,
|
|
800
|
-
sessionName: String(Date.now()),
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
if (!attached) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
} catch (err) {
|
|
807
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
808
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
809
|
-
if (!(err instanceof Error) || !err.message.startsWith('Workspace scripts failed during')) {
|
|
810
|
-
flow.showMessage({
|
|
811
|
-
title: 'Workspace Failed',
|
|
812
|
-
message: err instanceof Error ? err.message : 'Failed to create workspace',
|
|
813
|
-
variant: 'error',
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}, [attachLocal, currentProject, flow, refreshWorkspaces]);
|
|
818
|
-
|
|
819
|
-
// Handle selecting a source (branch/linear/manual)
|
|
820
|
-
const handleSourceSelect = useCallback(async (source: WorkspaceSource) => {
|
|
821
|
-
if (!currentProject) return;
|
|
822
|
-
|
|
823
|
-
if (source === 'branch') {
|
|
824
|
-
setWorkspaceFlow({ type: 'loading', title: 'Loading', message: 'Fetching remote branches...' });
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
const baseDir = getProjectBaseDir(currentProject);
|
|
828
|
-
const config = readProjectConfig(currentProject);
|
|
829
|
-
const allBranches = await listRemoteBranches(baseDir);
|
|
830
|
-
const branches = allBranches.filter(b => b !== config.baseBranch);
|
|
831
|
-
|
|
832
|
-
if (branches.length === 0) {
|
|
833
|
-
flow.showMessage({
|
|
834
|
-
title: 'No Branches',
|
|
835
|
-
message: `No remote branches found (excluding base branch ${config.baseBranch})`,
|
|
836
|
-
variant: 'warning',
|
|
837
|
-
});
|
|
838
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
setWorkspaceFlow({ type: 'branch-select', branches, selectedIndex: 0 });
|
|
843
|
-
} catch (err) {
|
|
844
|
-
flow.showMessage({
|
|
845
|
-
title: 'Error',
|
|
846
|
-
message: err instanceof Error ? err.message : 'Failed to fetch branches',
|
|
847
|
-
variant: 'error',
|
|
848
|
-
});
|
|
849
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
850
|
-
}
|
|
851
|
-
} else if (source === 'linear') {
|
|
852
|
-
const linearConfig = await getLinearConfig(currentProject);
|
|
853
|
-
if (!linearConfig.apiKey || linearConfig.teamKeys.length === 0) {
|
|
854
|
-
flow.showMessage({
|
|
855
|
-
title: 'Not Configured',
|
|
856
|
-
message: "Linear is not configured. Run 'gssh linear setup' to configure.",
|
|
857
|
-
variant: 'warning',
|
|
858
|
-
});
|
|
859
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
setWorkspaceFlow({ type: 'loading', title: 'Loading', message: 'Fetching Linear issues...' });
|
|
864
|
-
|
|
865
|
-
try {
|
|
866
|
-
// Fetch issues from first configured team
|
|
867
|
-
// Note: teamKeys[0] is guaranteed to exist due to the length check above
|
|
868
|
-
const teamKey = linearConfig.teamKeys[0];
|
|
869
|
-
if (!teamKey) {
|
|
870
|
-
// Defensive check - should never happen due to earlier length check
|
|
871
|
-
throw new Error('No team key available');
|
|
872
|
-
}
|
|
873
|
-
const issues = await fetchUnstartedIssues(linearConfig.apiKey, teamKey);
|
|
874
|
-
|
|
875
|
-
if (issues.length === 0) {
|
|
876
|
-
flow.showMessage({
|
|
877
|
-
title: 'No Issues',
|
|
878
|
-
message: 'No unstarted Linear issues found',
|
|
879
|
-
variant: 'warning',
|
|
880
|
-
});
|
|
881
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
setWorkspaceFlow({ type: 'linear-select', issues, selectedIndex: 0 });
|
|
886
|
-
} catch (err) {
|
|
887
|
-
flow.showMessage({
|
|
888
|
-
title: 'Error',
|
|
889
|
-
message: err instanceof Error ? err.message : 'Failed to fetch Linear issues',
|
|
890
|
-
variant: 'error',
|
|
891
|
-
});
|
|
892
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
893
|
-
}
|
|
894
|
-
} else if (source === 'manual') {
|
|
895
|
-
setWorkspaceFlow({ type: 'manual-name-input', inputValue: '', error: null });
|
|
896
|
-
}
|
|
897
|
-
}, [currentProject, flow]);
|
|
898
|
-
|
|
899
|
-
// Handle branch selection
|
|
900
|
-
const handleBranchSelect = useCallback(async (branch: string) => {
|
|
901
|
-
const workspaceName = sanitizeForFileSystem(branch);
|
|
902
|
-
await createWorkspaceAndOpenSession(workspaceName, branch, true);
|
|
903
|
-
}, [createWorkspaceAndOpenSession]);
|
|
904
|
-
|
|
905
|
-
// Handle Linear issue selection
|
|
906
|
-
const handleLinearSelect = useCallback(async (issue: LinearIssue) => {
|
|
907
|
-
const workspaceName = generateWorkspaceName(issue.identifier, issue.title);
|
|
908
|
-
await createWorkspaceAndOpenSession(workspaceName, workspaceName, false, issue);
|
|
909
|
-
}, [createWorkspaceAndOpenSession]);
|
|
910
|
-
|
|
911
|
-
// Handle manual workspace name submission (advances to branch input)
|
|
912
|
-
// Accepts branch-like names (e.g., fix/bla-bla-blah) and sanitizes them for workspace name
|
|
913
|
-
const handleManualNameSubmit = useCallback((name: string) => {
|
|
914
|
-
const trimmedName = name.trim();
|
|
915
|
-
if (!trimmedName) {
|
|
916
|
-
setWorkspaceFlow(prev => prev.type === 'manual-name-input' ? { ...prev, error: 'Workspace name is required' } : prev);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
// Sanitize the input to create a valid workspace name (converts slashes to hyphens, etc.)
|
|
920
|
-
const sanitizedName = sanitizeForFileSystem(trimmedName);
|
|
921
|
-
if (!sanitizedName) {
|
|
922
|
-
setWorkspaceFlow(prev => prev.type === 'manual-name-input' ? { ...prev, error: 'Name must contain at least one letter or number' } : prev);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
// Validate it can be used as a branch name (no spaces, special chars, etc.)
|
|
926
|
-
if (!isValidBranchName(trimmedName)) {
|
|
927
|
-
setWorkspaceFlow(prev => prev.type === 'manual-name-input' ? { ...prev, error: 'Invalid branch name (no spaces, .., or special chars like : ? * [ \\ ~)' } : prev);
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
// Advance to branch input step, pre-fill with original input (allows branch names with slashes)
|
|
931
|
-
setWorkspaceFlow({
|
|
932
|
-
type: 'manual-branch-input',
|
|
933
|
-
workspaceName: sanitizedName,
|
|
934
|
-
inputValue: trimmedName,
|
|
935
|
-
error: null,
|
|
936
|
-
});
|
|
937
|
-
}, []);
|
|
938
|
-
|
|
939
|
-
// Handle manual branch name submission (creates the workspace)
|
|
940
|
-
const handleManualBranchSubmit = useCallback(async (workspaceName: string, branchName: string) => {
|
|
941
|
-
const finalBranch = branchName.trim() || workspaceName;
|
|
942
|
-
if (!isValidBranchName(finalBranch)) {
|
|
943
|
-
setWorkspaceFlow(prev => prev.type === 'manual-branch-input' ? { ...prev, error: 'Invalid branch name (no spaces, .., or special chars like : ? * [ \\ ~)' } : prev);
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
await createWorkspaceAndOpenSession(workspaceName, finalBranch, false);
|
|
947
|
-
}, [createWorkspaceAndOpenSession]);
|
|
948
|
-
|
|
949
|
-
// Main handler to start new workspace flow
|
|
950
|
-
const handleNewWorkspaceFlow = useCallback(async () => {
|
|
951
|
-
if (!currentProject) return;
|
|
952
|
-
|
|
953
|
-
const linearConfig = await getLinearConfig(currentProject);
|
|
954
|
-
const hasLinear = linearConfig.apiKey !== null && linearConfig.teamKeys.length > 0;
|
|
955
|
-
|
|
956
|
-
const options: Array<{ label: string; description: string; value: WorkspaceSource }> = [
|
|
957
|
-
{ label: 'GitHub Branch', description: 'Create from existing remote branch', value: 'branch' },
|
|
958
|
-
...(hasLinear ? [{ label: 'Linear Issue', description: 'Create from Linear ticket', value: 'linear' as const }] : []),
|
|
959
|
-
{ label: 'Manual Name', description: 'Enter a custom workspace name', value: 'manual' },
|
|
960
|
-
];
|
|
961
|
-
|
|
962
|
-
setWorkspaceFlow({ type: 'source-select', selectedIndex: 0, options });
|
|
963
|
-
}, [currentProject]);
|
|
964
|
-
|
|
965
|
-
// ========== Project Creation (Custom State Machine) ==========
|
|
966
|
-
|
|
967
|
-
// Finalize project creation
|
|
968
|
-
const finalizeProject = useCallback(async (projectName: string) => {
|
|
969
|
-
setCurrentProject(projectName);
|
|
970
|
-
await refreshProjects();
|
|
971
|
-
setProjectFlow({ type: 'closed' });
|
|
972
|
-
flow.showMessage({
|
|
973
|
-
title: 'Project Created',
|
|
974
|
-
message: `Project "${projectName}" has been created successfully!`,
|
|
975
|
-
variant: 'success',
|
|
976
|
-
});
|
|
977
|
-
}, [refreshProjects, flow]);
|
|
978
|
-
|
|
979
|
-
// Check if a command exists (for onboarding confirm steps)
|
|
980
|
-
const checkCommand = useCallback(
|
|
981
|
-
(command: string) => checkCommandExists(command),
|
|
982
|
-
[]
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
// Advance to the next onboarding step
|
|
986
|
-
const advanceOnboardingStep = useCallback(async () => {
|
|
987
|
-
if (projectFlow.type !== 'onboarding') return;
|
|
988
|
-
|
|
989
|
-
const currentStep = projectFlow.steps[projectFlow.currentStep];
|
|
990
|
-
const newValues = { ...projectFlow.collectedValues };
|
|
991
|
-
const newSecrets = { ...projectFlow.collectedSecrets };
|
|
992
|
-
|
|
993
|
-
// Save current step's value if applicable
|
|
994
|
-
if (currentStep && (currentStep.type === 'input' || currentStep.type === 'secret')) {
|
|
995
|
-
const stepWithKey = currentStep as { configKey: string; defaultValue?: string };
|
|
996
|
-
const value = projectFlow.inputValue.trim() || stepWithKey.defaultValue || '';
|
|
997
|
-
|
|
998
|
-
if (currentStep.type === 'secret') {
|
|
999
|
-
newSecrets[stepWithKey.configKey] = value;
|
|
1000
|
-
} else {
|
|
1001
|
-
newValues[stepWithKey.configKey] = value;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const nextStepIndex = projectFlow.currentStep + 1;
|
|
1006
|
-
|
|
1007
|
-
if (nextStepIndex >= projectFlow.steps.length) {
|
|
1008
|
-
// All steps done - create the project
|
|
1009
|
-
setProjectFlow({ type: 'creating', projectName: projectFlow.projectName });
|
|
1010
|
-
|
|
1011
|
-
try {
|
|
1012
|
-
createProject(projectFlow.projectName, projectFlow.repo, projectFlow.baseBranch);
|
|
1013
|
-
|
|
1014
|
-
await applyProjectBundleState({
|
|
1015
|
-
projectName: projectFlow.projectName,
|
|
1016
|
-
bundle: {
|
|
1017
|
-
version: '1.0',
|
|
1018
|
-
name: projectFlow.bundleName,
|
|
1019
|
-
onboarding: projectFlow.steps,
|
|
1020
|
-
},
|
|
1021
|
-
inputValues: newValues,
|
|
1022
|
-
secretValues: newSecrets,
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
await finalizeProject(projectFlow.projectName);
|
|
1026
|
-
} catch (err) {
|
|
1027
|
-
flow.showMessage({
|
|
1028
|
-
title: 'Error',
|
|
1029
|
-
message: err instanceof Error ? err.message : 'Failed to create project',
|
|
1030
|
-
variant: 'error',
|
|
1031
|
-
});
|
|
1032
|
-
setProjectFlow({ type: 'closed' });
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
// Move to next step
|
|
1036
|
-
const nextStep = projectFlow.steps[nextStepIndex];
|
|
1037
|
-
|
|
1038
|
-
// If it's a confirm step with checkCommand, start checking
|
|
1039
|
-
if (nextStep.type === 'confirm' && (nextStep as { checkCommand?: string }).checkCommand) {
|
|
1040
|
-
setProjectFlow({
|
|
1041
|
-
...projectFlow,
|
|
1042
|
-
currentStep: nextStepIndex,
|
|
1043
|
-
collectedValues: newValues,
|
|
1044
|
-
collectedSecrets: newSecrets,
|
|
1045
|
-
inputValue: '',
|
|
1046
|
-
confirmStatus: 'checking',
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
const found = await checkCommand((nextStep as { checkCommand: string }).checkCommand);
|
|
1050
|
-
setProjectFlow(prev =>
|
|
1051
|
-
prev.type === 'onboarding'
|
|
1052
|
-
? { ...prev, confirmStatus: found ? 'found' : 'missing' }
|
|
1053
|
-
: prev
|
|
1054
|
-
);
|
|
1055
|
-
} else {
|
|
1056
|
-
const defaultValue = (nextStep as { defaultValue?: string }).defaultValue || '';
|
|
1057
|
-
setProjectFlow({
|
|
1058
|
-
...projectFlow,
|
|
1059
|
-
currentStep: nextStepIndex,
|
|
1060
|
-
collectedValues: newValues,
|
|
1061
|
-
collectedSecrets: newSecrets,
|
|
1062
|
-
inputValue: defaultValue,
|
|
1063
|
-
confirmStatus: null,
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}, [projectFlow, checkCommand, finalizeProject, flow]);
|
|
1068
|
-
|
|
1069
|
-
// Handle repository selection
|
|
1070
|
-
const handleSelectRepo = useCallback(async (repo: string) => {
|
|
1071
|
-
const projectName = extractRepoName(repo);
|
|
1072
|
-
|
|
1073
|
-
// Check if project already exists
|
|
1074
|
-
if (projectExists(projectName)) {
|
|
1075
|
-
flow.showMessage({
|
|
1076
|
-
title: 'Project Exists',
|
|
1077
|
-
message: `Project "${projectName}" already exists`,
|
|
1078
|
-
variant: 'error',
|
|
1079
|
-
});
|
|
1080
|
-
setProjectFlow({ type: 'closed' });
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
setProjectFlow({ type: 'cloning', repo });
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
const baseDir = getProjectBaseDir(projectName);
|
|
1088
|
-
await cloneRepository(repo, baseDir);
|
|
1089
|
-
const baseBranch = await getDefaultBranch(baseDir);
|
|
1090
|
-
|
|
1091
|
-
// Check for bundle
|
|
1092
|
-
const bundleDir = detectBundleInRepo(baseDir);
|
|
1093
|
-
if (bundleDir) {
|
|
1094
|
-
const loadedBundle = loadBundleFromPath(bundleDir);
|
|
1095
|
-
|
|
1096
|
-
if (loadedBundle.bundle.onboarding && loadedBundle.bundle.onboarding.length > 0) {
|
|
1097
|
-
// Start onboarding flow
|
|
1098
|
-
const firstStep = loadedBundle.bundle.onboarding[0];
|
|
1099
|
-
const initialInputValue = (firstStep as { defaultValue?: string }).defaultValue || '';
|
|
1100
|
-
|
|
1101
|
-
// If first step is a confirm with checkCommand, start checking
|
|
1102
|
-
if (firstStep.type === 'confirm' && (firstStep as { checkCommand?: string }).checkCommand) {
|
|
1103
|
-
setProjectFlow({
|
|
1104
|
-
type: 'onboarding',
|
|
1105
|
-
repo,
|
|
1106
|
-
projectName,
|
|
1107
|
-
baseBranch,
|
|
1108
|
-
bundleDir: loadedBundle.bundleDir,
|
|
1109
|
-
bundleName: loadedBundle.bundle.name,
|
|
1110
|
-
steps: loadedBundle.bundle.onboarding,
|
|
1111
|
-
currentStep: 0,
|
|
1112
|
-
collectedValues: {},
|
|
1113
|
-
collectedSecrets: {},
|
|
1114
|
-
inputValue: '',
|
|
1115
|
-
confirmStatus: 'checking',
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
const found = await checkCommand((firstStep as { checkCommand: string }).checkCommand);
|
|
1119
|
-
setProjectFlow(prev =>
|
|
1120
|
-
prev.type === 'onboarding'
|
|
1121
|
-
? { ...prev, confirmStatus: found ? 'found' : 'missing' }
|
|
1122
|
-
: prev
|
|
1123
|
-
);
|
|
1124
|
-
} else {
|
|
1125
|
-
setProjectFlow({
|
|
1126
|
-
type: 'onboarding',
|
|
1127
|
-
repo,
|
|
1128
|
-
projectName,
|
|
1129
|
-
baseBranch,
|
|
1130
|
-
bundleDir: loadedBundle.bundleDir,
|
|
1131
|
-
bundleName: loadedBundle.bundle.name,
|
|
1132
|
-
steps: loadedBundle.bundle.onboarding,
|
|
1133
|
-
currentStep: 0,
|
|
1134
|
-
collectedValues: {},
|
|
1135
|
-
collectedSecrets: {},
|
|
1136
|
-
inputValue: initialInputValue,
|
|
1137
|
-
confirmStatus: null,
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// No onboarding, just create project (scripts are in workspace .gitspace/scripts/)
|
|
1144
|
-
createProject(projectName, repo, baseBranch);
|
|
1145
|
-
await applyProjectBundleState({
|
|
1146
|
-
projectName,
|
|
1147
|
-
bundle: loadedBundle.bundle,
|
|
1148
|
-
});
|
|
1149
|
-
} else {
|
|
1150
|
-
// No bundle, just create project
|
|
1151
|
-
createProject(projectName, repo, baseBranch);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
await finalizeProject(projectName);
|
|
1155
|
-
} catch (err) {
|
|
1156
|
-
flow.showMessage({
|
|
1157
|
-
title: 'Error',
|
|
1158
|
-
message: err instanceof Error ? err.message : 'Failed to clone repository',
|
|
1159
|
-
variant: 'error',
|
|
1160
|
-
});
|
|
1161
|
-
setProjectFlow({ type: 'closed' });
|
|
1162
|
-
}
|
|
1163
|
-
}, [flow, checkCommand, finalizeProject]);
|
|
1164
|
-
|
|
1165
|
-
// Start new project flow
|
|
1166
|
-
const handleNewProjectFlow = useCallback(async () => {
|
|
1167
|
-
setProjectFlow({ type: 'loading-repos' });
|
|
1168
|
-
|
|
1169
|
-
try {
|
|
1170
|
-
const repos = await listAllRepos();
|
|
1171
|
-
|
|
1172
|
-
if (repos.length === 0) {
|
|
1173
|
-
flow.showMessage({
|
|
1174
|
-
title: 'No Repositories',
|
|
1175
|
-
message: 'No GitHub repositories found. Make sure you are logged in with `gh auth login`.',
|
|
1176
|
-
variant: 'warning',
|
|
1177
|
-
});
|
|
1178
|
-
setProjectFlow({ type: 'closed' });
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
setProjectFlow({ type: 'repo-select', repos, selectedIndex: 0 });
|
|
1183
|
-
} catch (err) {
|
|
1184
|
-
flow.showMessage({
|
|
1185
|
-
title: 'Error',
|
|
1186
|
-
message: err instanceof Error ? err.message : 'Failed to fetch repositories',
|
|
1187
|
-
variant: 'error',
|
|
1188
|
-
});
|
|
1189
|
-
setProjectFlow({ type: 'closed' });
|
|
1190
|
-
}
|
|
1191
|
-
}, [flow]);
|
|
1192
|
-
|
|
1193
|
-
// ========== Shared Hooks ==========
|
|
1194
|
-
|
|
1195
|
-
// Convert local backend state into panel data.
|
|
1196
|
-
const projectInfos: ProjectInfo[] = localProjects.map((project) => ({
|
|
1197
|
-
name: project.name,
|
|
1198
|
-
repository: project.repository,
|
|
1199
|
-
workspaceCount: project.workspaceCount,
|
|
1200
|
-
isCurrent: project.name === currentProject,
|
|
1201
|
-
}));
|
|
1202
|
-
|
|
1203
|
-
const workspaceInfos = localWorkspaces
|
|
1204
|
-
.filter((workspace) => workspace.projectName === currentProject)
|
|
1205
|
-
.map((workspace) => ({
|
|
1206
|
-
id: workspace.id,
|
|
1207
|
-
name: workspace.name,
|
|
1208
|
-
path: workspace.path,
|
|
1209
|
-
projectName: workspace.projectName,
|
|
1210
|
-
branch: workspace.branch,
|
|
1211
|
-
sessionCount: localSessions.filter((session) => session.workspaceId === workspace.id).length,
|
|
1212
|
-
isStale: workspace.isStale,
|
|
1213
|
-
processes: workspace.processes,
|
|
1214
|
-
processConfigError: workspace.processConfigError,
|
|
1215
|
-
serveDomain: workspace.serveDomain,
|
|
1216
|
-
}));
|
|
1217
|
-
|
|
1218
|
-
const sessionInfos = currentProject
|
|
1219
|
-
? localSessions.filter(
|
|
1220
|
-
(session) =>
|
|
1221
|
-
session.workspaceId !== 'unknown' &&
|
|
1222
|
-
session.workspaceId.startsWith(`${currentProject}:`)
|
|
1223
|
-
)
|
|
1224
|
-
: [];
|
|
1225
|
-
|
|
1226
|
-
const inboxItems = localInbox as InboxItem[];
|
|
1227
|
-
const inboxUnreadCount = localInboxUnreadCount;
|
|
1228
|
-
|
|
1229
|
-
// Project list hook
|
|
1230
|
-
const projectListProps = useProjectList({
|
|
1231
|
-
projects: projectInfos,
|
|
1232
|
-
onSelect: handleSelectProject,
|
|
1233
|
-
onCreateNew: handleNewProjectFlow,
|
|
1234
|
-
onDelete: handleDeleteProject,
|
|
1235
|
-
onRefresh: refreshProjects,
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
const processActions = useProcessActions({
|
|
1239
|
-
sessions: sessionInfos,
|
|
1240
|
-
startProcess: startLocalProcess,
|
|
1241
|
-
stopProcess: stopLocalProcess,
|
|
1242
|
-
attachSession: handleAttachSession,
|
|
1243
|
-
onStartProcessError: (error) => {
|
|
1244
|
-
flow.showMessage({
|
|
1245
|
-
title: 'Process Start Failed',
|
|
1246
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1247
|
-
variant: 'error',
|
|
1248
|
-
});
|
|
1249
|
-
},
|
|
1250
|
-
onStopProcessError: (error) => {
|
|
1251
|
-
flow.showMessage({
|
|
1252
|
-
title: 'Process Stop Failed',
|
|
1253
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1254
|
-
variant: 'error',
|
|
1255
|
-
});
|
|
1256
|
-
},
|
|
1257
|
-
onStartProcessAttachError: (error) => {
|
|
1258
|
-
flow.showMessage({
|
|
1259
|
-
title: 'Process Start Failed',
|
|
1260
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1261
|
-
variant: 'error',
|
|
1262
|
-
});
|
|
1263
|
-
},
|
|
1264
|
-
onAttachError: (error) => {
|
|
1265
|
-
flow.showMessage({
|
|
1266
|
-
title: 'Attach Failed',
|
|
1267
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1268
|
-
variant: 'error',
|
|
1269
|
-
});
|
|
1270
|
-
},
|
|
1271
|
-
onAttachTimeout: (target) => {
|
|
1272
|
-
flow.showMessage({
|
|
1273
|
-
title: 'Attach Timeout',
|
|
1274
|
-
message: `Process started but no active session was found for ${target.processName}#${target.instance}.`,
|
|
1275
|
-
variant: 'warning',
|
|
1276
|
-
});
|
|
1277
|
-
},
|
|
1278
|
-
onStartProcessAttachFinally: () => {
|
|
1279
|
-
void refreshWorkspaces();
|
|
1280
|
-
},
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
const handleStartProcess = processActions.handleStartProcess;
|
|
1284
|
-
const handleStartProcessAttach = processActions.handleStartProcessAttach;
|
|
1285
|
-
const handleStopProcess = processActions.handleStopProcess;
|
|
1286
|
-
|
|
1287
|
-
const handleProcessDisabled = useCallback((params: { workspaceId: string; processName: string }) => {
|
|
1288
|
-
const workspace = localWorkspaces.find((item) => item.id === params.workspaceId);
|
|
1289
|
-
const workspaceLabel = workspace?.name ?? params.workspaceId;
|
|
1290
|
-
toast.error(`Process "${params.processName}" is disabled in ${workspaceLabel} (instances: 0).`);
|
|
1291
|
-
}, [localWorkspaces]);
|
|
1292
|
-
|
|
1293
|
-
const handleOpenEvents = useCallback((workspaceId: string) => {
|
|
1294
|
-
setEventsWorkspaceId(workspaceId);
|
|
1295
|
-
// Find workspace path for events request
|
|
1296
|
-
const workspace = localWorkspaces.find(w => w.id === workspaceId);
|
|
1297
|
-
if (workspace) {
|
|
1298
|
-
void requestLocalEvents(workspace.path);
|
|
1299
|
-
}
|
|
1300
|
-
dispatch({ type: 'SET_VIEW', view: 'events' });
|
|
1301
|
-
}, [localWorkspaces, requestLocalEvents]);
|
|
1302
|
-
|
|
1303
|
-
// Spaces browser hook
|
|
1304
|
-
const spacesBrowserProps = useSpacesBrowser({
|
|
1305
|
-
workspaces: workspaceInfos,
|
|
1306
|
-
sessions: sessionInfos,
|
|
1307
|
-
onRequestSessions: () => {}, // Sessions already loaded
|
|
1308
|
-
onAttachSession: handleAttachSession,
|
|
1309
|
-
onEditProcesses: handleEditProcesses,
|
|
1310
|
-
onStartProcess: handleStartProcess,
|
|
1311
|
-
onStartProcessAttach: handleStartProcessAttach,
|
|
1312
|
-
onStopProcess: handleStopProcess,
|
|
1313
|
-
onProcessDisabled: handleProcessDisabled,
|
|
1314
|
-
onOpenEvents: handleOpenEvents,
|
|
1315
|
-
onRefresh: refreshWorkspaces,
|
|
1316
|
-
onBack: () => dispatch({ type: 'SET_PANEL_FOCUS', focus: 'projects' }),
|
|
1317
|
-
onCreateWorkspace: handleNewWorkspaceFlow,
|
|
1318
|
-
machineName: currentProject || undefined,
|
|
1319
|
-
showProjectHeaders: false, // Don't show project headers since we're already filtered
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
// Machine list hook (for remote mode)
|
|
1323
|
-
const machineListProps = useMachineList({
|
|
1324
|
-
machines: remoteMachines.machines,
|
|
1325
|
-
status: remoteMachines.status,
|
|
1326
|
-
error: remoteMachines.error,
|
|
1327
|
-
publicKey: undefined,
|
|
1328
|
-
onConnect: async (machine) => {
|
|
1329
|
-
dispatch({ type: 'SET_MACHINE', machine });
|
|
1330
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1331
|
-
},
|
|
1332
|
-
onRefresh: remoteMachines.refreshMachines,
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
// Inbox hook
|
|
1336
|
-
const inboxProps = useInbox({
|
|
1337
|
-
items: inboxItems,
|
|
1338
|
-
unreadCount: inboxUnreadCount,
|
|
1339
|
-
onClearItem: async (id) => {
|
|
1340
|
-
await clearLocalInbox(id);
|
|
1341
|
-
await refreshInbox();
|
|
1342
|
-
},
|
|
1343
|
-
onClearAll: async () => {
|
|
1344
|
-
await clearLocalInbox();
|
|
1345
|
-
await refreshInbox();
|
|
1346
|
-
},
|
|
1347
|
-
onMarkRead: async (id) => {
|
|
1348
|
-
await markLocalInboxRead(id);
|
|
1349
|
-
await refreshInbox();
|
|
1350
|
-
},
|
|
1351
|
-
onAttachSession: async (sessionId) => {
|
|
1352
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1353
|
-
await handleAttachSession({ sessionId });
|
|
1354
|
-
},
|
|
1355
|
-
onClose: () => {
|
|
1356
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1357
|
-
},
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
// Events hook
|
|
1361
|
-
const eventsItems: WideEventItem[] = localEvents.map(toWideEventItem);
|
|
1362
|
-
|
|
1363
|
-
const eventsProps = useEvents({
|
|
1364
|
-
events: eventsItems,
|
|
1365
|
-
liveEventIds: localLiveEventIds,
|
|
1366
|
-
savedFilters: localSavedEventFilters,
|
|
1367
|
-
onSelectFilter: (filter) => {
|
|
1368
|
-
if (!eventsWorkspaceId) return;
|
|
1369
|
-
const workspace = localWorkspaces.find(w => w.id === eventsWorkspaceId);
|
|
1370
|
-
if (!workspace) return;
|
|
1371
|
-
if (filter) {
|
|
1372
|
-
const sinceMs = filter.sinceMinutes
|
|
1373
|
-
? Date.now() - filter.sinceMinutes * 60 * 1000
|
|
1374
|
-
: undefined;
|
|
1375
|
-
void requestLocalEvents(
|
|
1376
|
-
workspace.path,
|
|
1377
|
-
filter.filter as WideEventFilter,
|
|
1378
|
-
undefined,
|
|
1379
|
-
sinceMs
|
|
1380
|
-
);
|
|
1381
|
-
} else {
|
|
1382
|
-
void requestLocalEvents(workspace.path);
|
|
1383
|
-
}
|
|
1384
|
-
},
|
|
1385
|
-
onClose: () => {
|
|
1386
|
-
setEventsWorkspaceId(null);
|
|
1387
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1388
|
-
},
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
// Events polling when events view is active
|
|
1392
|
-
useEffect(() => {
|
|
1393
|
-
if (state.view !== 'events' || !eventsWorkspaceId) return;
|
|
1394
|
-
const workspace = localWorkspaces.find(w => w.id === eventsWorkspaceId);
|
|
1395
|
-
if (!workspace) return;
|
|
1396
|
-
|
|
1397
|
-
const interval = setInterval(() => {
|
|
1398
|
-
const activeFilter = eventsProps.activeFilterName
|
|
1399
|
-
? localSavedEventFilters.find((filter) => filter.name === eventsProps.activeFilterName) ?? null
|
|
1400
|
-
: null;
|
|
1401
|
-
|
|
1402
|
-
if (activeFilter) {
|
|
1403
|
-
const sinceMs = activeFilter.sinceMinutes
|
|
1404
|
-
? Date.now() - activeFilter.sinceMinutes * 60 * 1000
|
|
1405
|
-
: undefined;
|
|
1406
|
-
void requestLocalEvents(
|
|
1407
|
-
workspace.path,
|
|
1408
|
-
activeFilter.filter as WideEventFilter,
|
|
1409
|
-
undefined,
|
|
1410
|
-
sinceMs
|
|
1411
|
-
);
|
|
1412
|
-
} else {
|
|
1413
|
-
void requestLocalEvents(workspace.path);
|
|
1414
|
-
}
|
|
1415
|
-
}, 2000);
|
|
1416
|
-
|
|
1417
|
-
return () => clearInterval(interval);
|
|
1418
|
-
}, [
|
|
1419
|
-
state.view,
|
|
1420
|
-
eventsWorkspaceId,
|
|
1421
|
-
localWorkspaces,
|
|
1422
|
-
eventsProps.activeFilterName,
|
|
1423
|
-
localSavedEventFilters,
|
|
1424
|
-
requestLocalEvents,
|
|
1425
|
-
]);
|
|
1426
|
-
|
|
1427
|
-
// ========== Activity Tracking for Notifications ==========
|
|
1428
|
-
|
|
1429
|
-
const holdWhenIdleMs = notificationConfig.toast.holdWhenIdleMs ?? 15000;
|
|
1430
|
-
const { isUserActive, markActivity: handleTerminalActivity } = useUserActivity({
|
|
1431
|
-
isActivityTracked: state.view === 'terminal',
|
|
1432
|
-
holdWhenIdleMs,
|
|
1433
|
-
});
|
|
1434
|
-
|
|
1435
|
-
// ========== Notification Toasts ==========
|
|
1436
|
-
|
|
1437
|
-
const handleShowToast = useCallback((notification: ToastNotification) => {
|
|
1438
|
-
const description = notification.preview
|
|
1439
|
-
? `${notification.preview} · [Shift+Tab to attach]`
|
|
1440
|
-
: '[Shift+Tab to attach]';
|
|
1441
|
-
toast.info(`${notification.icon} ${notification.title}`, {
|
|
1442
|
-
description,
|
|
1443
|
-
duration: 8000,
|
|
1444
|
-
});
|
|
1445
|
-
}, []);
|
|
1446
|
-
|
|
1447
|
-
const notifications = useNotifications({
|
|
1448
|
-
items: inboxItems,
|
|
1449
|
-
config: notificationConfig,
|
|
1450
|
-
onShowToast: handleShowToast,
|
|
1451
|
-
onAttachSession: (sessionId) => {
|
|
1452
|
-
void handleAttachSession({ sessionId }).catch((error) => {
|
|
1453
|
-
flow.showMessage({
|
|
1454
|
-
title: 'Attach Failed',
|
|
1455
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1456
|
-
variant: 'error',
|
|
1457
|
-
});
|
|
1458
|
-
});
|
|
1459
|
-
},
|
|
1460
|
-
onMarkRead: async (itemId) => {
|
|
1461
|
-
await markLocalInboxRead(itemId);
|
|
1462
|
-
await refreshInbox();
|
|
1463
|
-
},
|
|
1464
|
-
pollIntervalMs: 5000,
|
|
1465
|
-
onRefreshInbox: refreshInbox,
|
|
1466
|
-
isUserActive,
|
|
1467
|
-
currentSessionId: localAttachedSessionId ?? undefined,
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
useEffect(() => {
|
|
1471
|
-
if (state.view !== 'scripts') {
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
setLocalWriteCallback((data) => {
|
|
1476
|
-
scriptTerminalRef.current?.feed(data);
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
return () => {
|
|
1480
|
-
setLocalWriteCallback(null);
|
|
1481
|
-
};
|
|
1482
|
-
}, [setLocalWriteCallback, state.view]);
|
|
1483
|
-
|
|
1484
|
-
useEffect(() => {
|
|
1485
|
-
const handlePaste = (event: PasteEvent) => {
|
|
1486
|
-
const text = normalizeInputText(event.text ?? '');
|
|
1487
|
-
if (!text) {
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
if (flow.isOpen) {
|
|
1492
|
-
const isWizardTextStep =
|
|
1493
|
-
isFlowWizard(flow.flow) &&
|
|
1494
|
-
(() => {
|
|
1495
|
-
const step = flow.flow.steps[flow.flow.currentStep];
|
|
1496
|
-
return step?.type === 'input' || step?.type === 'secret';
|
|
1497
|
-
})();
|
|
1498
|
-
|
|
1499
|
-
if (isFlowInput(flow.flow) || isFlowConfirmTyped(flow.flow) || isWizardTextStep) {
|
|
1500
|
-
const current = 'inputValue' in flow.flow ? flow.flow.inputValue || '' : '';
|
|
1501
|
-
flow.handleInput(current + text);
|
|
1502
|
-
event.preventDefault();
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
if (projectFlow.type === 'onboarding') {
|
|
1508
|
-
const step = projectFlow.steps[projectFlow.currentStep];
|
|
1509
|
-
if (step?.type === 'input' || step?.type === 'secret') {
|
|
1510
|
-
setProjectFlow({
|
|
1511
|
-
...projectFlow,
|
|
1512
|
-
inputValue: projectFlow.inputValue + text,
|
|
1513
|
-
});
|
|
1514
|
-
event.preventDefault();
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
if (workspaceFlow.type === 'manual-name-input') {
|
|
1520
|
-
setWorkspaceFlow({
|
|
1521
|
-
...workspaceFlow,
|
|
1522
|
-
inputValue: workspaceFlow.inputValue + text,
|
|
1523
|
-
error: null,
|
|
1524
|
-
});
|
|
1525
|
-
event.preventDefault();
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
if (workspaceFlow.type === 'manual-branch-input') {
|
|
1530
|
-
setWorkspaceFlow({
|
|
1531
|
-
...workspaceFlow,
|
|
1532
|
-
inputValue: workspaceFlow.inputValue + text,
|
|
1533
|
-
error: null,
|
|
1534
|
-
});
|
|
1535
|
-
event.preventDefault();
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (settingsFlow.type === 'edit-duration' || settingsFlow.type === 'edit-hold-duration') {
|
|
1540
|
-
const digits = getNumericInputChunk(text);
|
|
1541
|
-
if (!digits) {
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
setSettingsFlow({
|
|
1545
|
-
...settingsFlow,
|
|
1546
|
-
value: settingsFlow.value + digits,
|
|
1547
|
-
});
|
|
1548
|
-
event.preventDefault();
|
|
1549
|
-
}
|
|
1550
|
-
};
|
|
1551
|
-
|
|
1552
|
-
renderer.keyInput.on('paste', handlePaste);
|
|
1553
|
-
return () => {
|
|
1554
|
-
renderer.keyInput.off('paste', handlePaste);
|
|
1555
|
-
};
|
|
1556
|
-
}, [flow, projectFlow, renderer, settingsFlow, workspaceFlow]);
|
|
1557
|
-
|
|
1558
|
-
// ========== Keyboard Handlers ==========
|
|
1559
|
-
|
|
1560
|
-
useKeyboard(async (key) => {
|
|
1561
|
-
const localScriptTerminalRunning =
|
|
1562
|
-
state.view === 'scripts' &&
|
|
1563
|
-
(localScriptState?.isRunning ?? true);
|
|
1564
|
-
|
|
1565
|
-
// Handle flow modals FIRST - even in terminal view
|
|
1566
|
-
// This ensures y/n work in confirmation modals when terminal is underneath
|
|
1567
|
-
if (flow.isOpen && !localScriptTerminalRunning) {
|
|
1568
|
-
// Handle confirm modal with y/n shortcuts
|
|
1569
|
-
if (flow.flow.type === 'confirm') {
|
|
1570
|
-
if (key.raw === 'y' || key.name === 'return') {
|
|
1571
|
-
await flow.handleConfirm();
|
|
1572
|
-
} else if (key.raw === 'n' || key.name === 'escape') {
|
|
1573
|
-
flow.handleCancel();
|
|
1574
|
-
}
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
// Handle text input modals (before j/k navigation)
|
|
1579
|
-
const isWizardTextStep =
|
|
1580
|
-
isFlowWizard(flow.flow) &&
|
|
1581
|
-
(() => {
|
|
1582
|
-
const step = flow.flow.steps[flow.flow.currentStep];
|
|
1583
|
-
return step?.type === 'input' || step?.type === 'secret';
|
|
1584
|
-
})();
|
|
1585
|
-
|
|
1586
|
-
if (isFlowInput(flow.flow) || isFlowConfirmTyped(flow.flow) || isWizardTextStep) {
|
|
1587
|
-
if (key.name === 'escape') {
|
|
1588
|
-
flow.handleCancel();
|
|
1589
|
-
} else if (key.name === 'return') {
|
|
1590
|
-
await flow.handleConfirm();
|
|
1591
|
-
} else if (key.name === 'backspace') {
|
|
1592
|
-
const current = 'inputValue' in flow.flow ? flow.flow.inputValue || '' : '';
|
|
1593
|
-
flow.handleInput(current.slice(0, -1));
|
|
1594
|
-
} else {
|
|
1595
|
-
const chunk = getKeyboardInputChunk(key);
|
|
1596
|
-
if (!chunk) {
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
const current = 'inputValue' in flow.flow ? flow.flow.inputValue || '' : '';
|
|
1600
|
-
flow.handleInput(current + chunk);
|
|
1601
|
-
}
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// Handle other modals (select, message, etc.)
|
|
1606
|
-
if (key.name === 'escape') {
|
|
1607
|
-
flow.handleCancel();
|
|
1608
|
-
} else if (key.name === 'return') {
|
|
1609
|
-
await flow.handleConfirm();
|
|
1610
|
-
} else if (key.name === 'up' || key.raw === 'k') {
|
|
1611
|
-
flow.moveUp();
|
|
1612
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1613
|
-
flow.moveDown();
|
|
1614
|
-
}
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
// Remote machine screen handles its own keyboard bindings.
|
|
1619
|
-
if (
|
|
1620
|
-
isRemoteMode &&
|
|
1621
|
-
state.view === 'projects' &&
|
|
1622
|
-
state.selectedMachine &&
|
|
1623
|
-
state.selectedMachine.machineId !== 'local'
|
|
1624
|
-
) {
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// Shift+Tab attach hotkey - check FIRST, even in terminal view
|
|
1629
|
-
// This allows attaching to a different session while in a terminal
|
|
1630
|
-
if (key.shift && key.name === 'tab' && notifications.activeToast) {
|
|
1631
|
-
// Show confirmation before switching sessions
|
|
1632
|
-
const sessionLabel = getSessionLabel(notifications.activeToast.sessionName);
|
|
1633
|
-
flow.showConfirm({
|
|
1634
|
-
title: 'Switch Session',
|
|
1635
|
-
message: `Switch to "${sessionLabel}"?`,
|
|
1636
|
-
confirmLabel: 'Switch',
|
|
1637
|
-
onConfirm: () => {
|
|
1638
|
-
notifications.attachToActiveToast();
|
|
1639
|
-
},
|
|
1640
|
-
});
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// Don't handle keys when in terminal view (Terminal component handles input)
|
|
1645
|
-
if (state.view === 'terminal') {
|
|
1646
|
-
return;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// Read-only script terminal view.
|
|
1650
|
-
if (state.view === 'scripts') {
|
|
1651
|
-
if (
|
|
1652
|
-
!localScriptState?.isRunning &&
|
|
1653
|
-
(
|
|
1654
|
-
key.name === 'escape' ||
|
|
1655
|
-
key.name === 'n' ||
|
|
1656
|
-
key.raw === 'n'
|
|
1657
|
-
)
|
|
1658
|
-
) {
|
|
1659
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1660
|
-
}
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// Events view keyboard handling
|
|
1665
|
-
if (state.view === 'events') {
|
|
1666
|
-
if (key.name === 'escape' || key.raw === 'q') {
|
|
1667
|
-
setEventsWorkspaceId(null);
|
|
1668
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
1669
|
-
} else if (key.name === 'up' || key.raw === 'k') {
|
|
1670
|
-
eventsProps.selectIndex(eventsProps.selectedIndex - 1);
|
|
1671
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1672
|
-
eventsProps.selectIndex(eventsProps.selectedIndex + 1);
|
|
1673
|
-
}
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// Handle project creation flow (custom state machine)
|
|
1678
|
-
if (projectFlow.type !== 'closed') {
|
|
1679
|
-
if (key.name === 'escape') {
|
|
1680
|
-
setProjectFlow({ type: 'closed' });
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
if (projectFlow.type === 'repo-select') {
|
|
1685
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1686
|
-
setProjectFlow({
|
|
1687
|
-
...projectFlow,
|
|
1688
|
-
selectedIndex: Math.max(0, projectFlow.selectedIndex - 1),
|
|
1689
|
-
});
|
|
1690
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1691
|
-
setProjectFlow({
|
|
1692
|
-
...projectFlow,
|
|
1693
|
-
selectedIndex: Math.min(projectFlow.repos.length - 1, projectFlow.selectedIndex + 1),
|
|
1694
|
-
});
|
|
1695
|
-
} else if (key.name === 'return') {
|
|
1696
|
-
const repo = projectFlow.repos[projectFlow.selectedIndex];
|
|
1697
|
-
if (repo) {
|
|
1698
|
-
await handleSelectRepo(repo);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
if (projectFlow.type === 'onboarding') {
|
|
1705
|
-
const step = projectFlow.steps[projectFlow.currentStep];
|
|
1706
|
-
|
|
1707
|
-
if (step.type === 'info' || step.type === 'confirm') {
|
|
1708
|
-
// For info/confirm steps, Enter to continue (if not checking)
|
|
1709
|
-
if (key.name === 'return' && projectFlow.confirmStatus !== 'checking') {
|
|
1710
|
-
await advanceOnboardingStep();
|
|
1711
|
-
}
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
if (step.type === 'input' || step.type === 'secret') {
|
|
1716
|
-
if (key.name === 'return') {
|
|
1717
|
-
await advanceOnboardingStep();
|
|
1718
|
-
} else if (key.name === 'backspace') {
|
|
1719
|
-
setProjectFlow({
|
|
1720
|
-
...projectFlow,
|
|
1721
|
-
inputValue: projectFlow.inputValue.slice(0, -1),
|
|
1722
|
-
});
|
|
1723
|
-
} else {
|
|
1724
|
-
const chunk = getKeyboardInputChunk(key);
|
|
1725
|
-
if (!chunk) {
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
setProjectFlow({
|
|
1729
|
-
...projectFlow,
|
|
1730
|
-
inputValue: projectFlow.inputValue + chunk,
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// For loading/cloning/creating states, just wait (escape to cancel handled above)
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
// Handle workspace creation flow (custom state machine)
|
|
1743
|
-
if (workspaceFlow.type !== 'closed') {
|
|
1744
|
-
if (key.name === 'escape') {
|
|
1745
|
-
setWorkspaceFlow({ type: 'closed' });
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
if (workspaceFlow.type === 'source-select') {
|
|
1750
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1751
|
-
setWorkspaceFlow({
|
|
1752
|
-
...workspaceFlow,
|
|
1753
|
-
selectedIndex: Math.max(0, workspaceFlow.selectedIndex - 1),
|
|
1754
|
-
});
|
|
1755
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1756
|
-
setWorkspaceFlow({
|
|
1757
|
-
...workspaceFlow,
|
|
1758
|
-
selectedIndex: Math.min(workspaceFlow.options.length - 1, workspaceFlow.selectedIndex + 1),
|
|
1759
|
-
});
|
|
1760
|
-
} else if (key.name === 'return') {
|
|
1761
|
-
const selected = workspaceFlow.options[workspaceFlow.selectedIndex];
|
|
1762
|
-
if (selected) {
|
|
1763
|
-
await handleSourceSelect(selected.value);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
if (workspaceFlow.type === 'branch-select') {
|
|
1770
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1771
|
-
setWorkspaceFlow({
|
|
1772
|
-
...workspaceFlow,
|
|
1773
|
-
selectedIndex: Math.max(0, workspaceFlow.selectedIndex - 1),
|
|
1774
|
-
});
|
|
1775
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1776
|
-
setWorkspaceFlow({
|
|
1777
|
-
...workspaceFlow,
|
|
1778
|
-
selectedIndex: Math.min(workspaceFlow.branches.length - 1, workspaceFlow.selectedIndex + 1),
|
|
1779
|
-
});
|
|
1780
|
-
} else if (key.name === 'return') {
|
|
1781
|
-
const branch = workspaceFlow.branches[workspaceFlow.selectedIndex];
|
|
1782
|
-
if (branch) {
|
|
1783
|
-
await handleBranchSelect(branch);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
if (workspaceFlow.type === 'linear-select') {
|
|
1790
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1791
|
-
setWorkspaceFlow({
|
|
1792
|
-
...workspaceFlow,
|
|
1793
|
-
selectedIndex: Math.max(0, workspaceFlow.selectedIndex - 1),
|
|
1794
|
-
});
|
|
1795
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1796
|
-
setWorkspaceFlow({
|
|
1797
|
-
...workspaceFlow,
|
|
1798
|
-
selectedIndex: Math.min(workspaceFlow.issues.length - 1, workspaceFlow.selectedIndex + 1),
|
|
1799
|
-
});
|
|
1800
|
-
} else if (key.name === 'return') {
|
|
1801
|
-
const issue = workspaceFlow.issues[workspaceFlow.selectedIndex];
|
|
1802
|
-
if (issue) {
|
|
1803
|
-
await handleLinearSelect(issue);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
if (workspaceFlow.type === 'manual-name-input') {
|
|
1810
|
-
if (key.name === 'return') {
|
|
1811
|
-
handleManualNameSubmit(workspaceFlow.inputValue);
|
|
1812
|
-
} else if (key.name === 'backspace') {
|
|
1813
|
-
setWorkspaceFlow({
|
|
1814
|
-
...workspaceFlow,
|
|
1815
|
-
inputValue: workspaceFlow.inputValue.slice(0, -1),
|
|
1816
|
-
error: null,
|
|
1817
|
-
});
|
|
1818
|
-
} else {
|
|
1819
|
-
const chunk = getKeyboardInputChunk(key);
|
|
1820
|
-
if (!chunk) {
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
setWorkspaceFlow({
|
|
1824
|
-
...workspaceFlow,
|
|
1825
|
-
inputValue: workspaceFlow.inputValue + chunk,
|
|
1826
|
-
error: null,
|
|
1827
|
-
});
|
|
1828
|
-
}
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
if (workspaceFlow.type === 'manual-branch-input') {
|
|
1833
|
-
if (key.name === 'return') {
|
|
1834
|
-
await handleManualBranchSubmit(workspaceFlow.workspaceName, workspaceFlow.inputValue);
|
|
1835
|
-
} else if (key.name === 'backspace') {
|
|
1836
|
-
setWorkspaceFlow({
|
|
1837
|
-
...workspaceFlow,
|
|
1838
|
-
inputValue: workspaceFlow.inputValue.slice(0, -1),
|
|
1839
|
-
error: null,
|
|
1840
|
-
});
|
|
1841
|
-
} else {
|
|
1842
|
-
const chunk = getKeyboardInputChunk(key);
|
|
1843
|
-
if (!chunk) {
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
setWorkspaceFlow({
|
|
1847
|
-
...workspaceFlow,
|
|
1848
|
-
inputValue: workspaceFlow.inputValue + chunk,
|
|
1849
|
-
error: null,
|
|
1850
|
-
});
|
|
1851
|
-
}
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// For loading/creating states, just wait (escape to cancel handled above)
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
// Global shortcuts
|
|
1860
|
-
if (key.raw === '?') {
|
|
1861
|
-
flow.showHelp(getDefaultShortcuts());
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
if (key.raw === 'q' || (key.ctrl && key.raw === 'c')) {
|
|
1866
|
-
onQuit?.();
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
// Inbox shortcut (global) - open full-screen inbox view
|
|
1871
|
-
if (key.raw === 'i') {
|
|
1872
|
-
dispatch({ type: 'SET_VIEW', view: 'inbox' });
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// Settings shortcut (global) - open settings modal
|
|
1877
|
-
if (key.raw === ',') {
|
|
1878
|
-
const config = await localPreferencesService.getNotificationConfig();
|
|
1879
|
-
setSettingsFlow({ type: 'main-menu', selectedIndex: 0, config });
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
// Settings flow keyboard handling
|
|
1884
|
-
if (settingsFlow.type !== 'closed') {
|
|
1885
|
-
if (key.name === 'escape') {
|
|
1886
|
-
if (settingsFlow.type === 'types-menu') {
|
|
1887
|
-
// Go back to main menu
|
|
1888
|
-
setSettingsFlow({ type: 'main-menu', selectedIndex: 4, config: settingsFlow.config });
|
|
1889
|
-
} else if (settingsFlow.type === 'edit-duration' || settingsFlow.type === 'edit-hold-duration') {
|
|
1890
|
-
// Go back to main menu
|
|
1891
|
-
setSettingsFlow({ type: 'main-menu', selectedIndex: settingsFlow.type === 'edit-duration' ? 2 : 3, config: settingsFlow.config });
|
|
1892
|
-
} else {
|
|
1893
|
-
setSettingsFlow({ type: 'closed' });
|
|
1894
|
-
}
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
if (settingsFlow.type === 'main-menu') {
|
|
1899
|
-
const menuItems = [
|
|
1900
|
-
'notifications-enabled',
|
|
1901
|
-
'toast-enabled',
|
|
1902
|
-
'min-duration',
|
|
1903
|
-
'hold-duration',
|
|
1904
|
-
'types',
|
|
1905
|
-
'reset',
|
|
1906
|
-
];
|
|
1907
|
-
|
|
1908
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1909
|
-
setSettingsFlow({
|
|
1910
|
-
...settingsFlow,
|
|
1911
|
-
selectedIndex: Math.max(0, settingsFlow.selectedIndex - 1),
|
|
1912
|
-
});
|
|
1913
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1914
|
-
setSettingsFlow({
|
|
1915
|
-
...settingsFlow,
|
|
1916
|
-
selectedIndex: Math.min(menuItems.length - 1, settingsFlow.selectedIndex + 1),
|
|
1917
|
-
});
|
|
1918
|
-
} else if (key.name === 'return') {
|
|
1919
|
-
const selected = menuItems[settingsFlow.selectedIndex];
|
|
1920
|
-
if (selected === 'notifications-enabled') {
|
|
1921
|
-
const newConfig = { ...settingsFlow.config, enabled: !settingsFlow.config.enabled };
|
|
1922
|
-
await localPreferencesService.updateNotificationConfig(newConfig);
|
|
1923
|
-
setNotificationConfig(newConfig);
|
|
1924
|
-
setSettingsFlow({ ...settingsFlow, config: newConfig });
|
|
1925
|
-
} else if (selected === 'toast-enabled') {
|
|
1926
|
-
const newConfig = {
|
|
1927
|
-
...settingsFlow.config,
|
|
1928
|
-
toast: { ...settingsFlow.config.toast, enabled: !settingsFlow.config.toast.enabled },
|
|
1929
|
-
};
|
|
1930
|
-
await localPreferencesService.updateNotificationConfig(newConfig);
|
|
1931
|
-
setNotificationConfig(newConfig);
|
|
1932
|
-
setSettingsFlow({ ...settingsFlow, config: newConfig });
|
|
1933
|
-
} else if (selected === 'min-duration') {
|
|
1934
|
-
const currentSec = Math.round(settingsFlow.config.minCommandDurationMs / 1000);
|
|
1935
|
-
setSettingsFlow({ type: 'edit-duration', value: String(currentSec), config: settingsFlow.config });
|
|
1936
|
-
} else if (selected === 'hold-duration') {
|
|
1937
|
-
const currentSec = Math.round(settingsFlow.config.toast.holdWhenIdleMs / 1000);
|
|
1938
|
-
setSettingsFlow({ type: 'edit-hold-duration', value: String(currentSec), config: settingsFlow.config });
|
|
1939
|
-
} else if (selected === 'types') {
|
|
1940
|
-
setSettingsFlow({ type: 'types-menu', selectedIndex: 0, config: settingsFlow.config });
|
|
1941
|
-
} else if (selected === 'reset') {
|
|
1942
|
-
await localPreferencesService.updateNotificationConfig(DEFAULT_NOTIFICATION_CONFIG);
|
|
1943
|
-
setNotificationConfig(DEFAULT_NOTIFICATION_CONFIG);
|
|
1944
|
-
setSettingsFlow({ ...settingsFlow, config: DEFAULT_NOTIFICATION_CONFIG });
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
if (settingsFlow.type === 'types-menu') {
|
|
1951
|
-
const typeKeys: (keyof NotificationTypeConfig)[] = ['exit', 'idle', 'bell', 'title', 'osc'];
|
|
1952
|
-
const menuItems = [...typeKeys, 'back'];
|
|
1953
|
-
|
|
1954
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
1955
|
-
setSettingsFlow({
|
|
1956
|
-
...settingsFlow,
|
|
1957
|
-
selectedIndex: Math.max(0, settingsFlow.selectedIndex - 1),
|
|
1958
|
-
});
|
|
1959
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
1960
|
-
setSettingsFlow({
|
|
1961
|
-
...settingsFlow,
|
|
1962
|
-
selectedIndex: Math.min(menuItems.length - 1, settingsFlow.selectedIndex + 1),
|
|
1963
|
-
});
|
|
1964
|
-
} else if (key.name === 'return') {
|
|
1965
|
-
if (settingsFlow.selectedIndex === menuItems.length - 1) {
|
|
1966
|
-
// Back
|
|
1967
|
-
setSettingsFlow({ type: 'main-menu', selectedIndex: 4, config: settingsFlow.config });
|
|
1968
|
-
} else {
|
|
1969
|
-
// Toggle type
|
|
1970
|
-
const typeKey = typeKeys[settingsFlow.selectedIndex];
|
|
1971
|
-
if (typeKey) {
|
|
1972
|
-
const newConfig = {
|
|
1973
|
-
...settingsFlow.config,
|
|
1974
|
-
types: { ...settingsFlow.config.types, [typeKey]: !settingsFlow.config.types[typeKey] },
|
|
1975
|
-
};
|
|
1976
|
-
await localPreferencesService.updateNotificationConfig(newConfig);
|
|
1977
|
-
setNotificationConfig(newConfig);
|
|
1978
|
-
setSettingsFlow({ ...settingsFlow, config: newConfig });
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
if (settingsFlow.type === 'edit-duration' || settingsFlow.type === 'edit-hold-duration') {
|
|
1986
|
-
if (key.name === 'return') {
|
|
1987
|
-
const num = parseInt(settingsFlow.value, 10);
|
|
1988
|
-
if (!isNaN(num) && num >= 0) {
|
|
1989
|
-
const newConfig = settingsFlow.type === 'edit-duration'
|
|
1990
|
-
? { ...settingsFlow.config, minCommandDurationMs: num * 1000 }
|
|
1991
|
-
: { ...settingsFlow.config, toast: { ...settingsFlow.config.toast, holdWhenIdleMs: num * 1000 } };
|
|
1992
|
-
await localPreferencesService.updateNotificationConfig(newConfig);
|
|
1993
|
-
setNotificationConfig(newConfig);
|
|
1994
|
-
setSettingsFlow({ type: 'main-menu', selectedIndex: settingsFlow.type === 'edit-duration' ? 2 : 3, config: newConfig });
|
|
1995
|
-
}
|
|
1996
|
-
} else if (key.name === 'backspace') {
|
|
1997
|
-
setSettingsFlow({
|
|
1998
|
-
...settingsFlow,
|
|
1999
|
-
value: settingsFlow.value.slice(0, -1),
|
|
2000
|
-
});
|
|
2001
|
-
} else {
|
|
2002
|
-
const chunk = getKeyboardInputChunk(key);
|
|
2003
|
-
if (!chunk) {
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
const digits = getNumericInputChunk(chunk);
|
|
2007
|
-
if (!digits) {
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
|
-
setSettingsFlow({
|
|
2011
|
-
...settingsFlow,
|
|
2012
|
-
value: settingsFlow.value + digits,
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
return;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// Inbox view keyboard handling
|
|
2022
|
-
if (state.view === 'inbox') {
|
|
2023
|
-
const command = resolveInboxCommand({
|
|
2024
|
-
name: key.name,
|
|
2025
|
-
raw: key.raw,
|
|
2026
|
-
shift: key.shift,
|
|
2027
|
-
});
|
|
2028
|
-
|
|
2029
|
-
if (command === 'back') {
|
|
2030
|
-
if (inboxProps.isViewingThread) {
|
|
2031
|
-
inboxProps.closeThread();
|
|
2032
|
-
} else {
|
|
2033
|
-
inboxProps.close();
|
|
2034
|
-
}
|
|
2035
|
-
} else if (command === 'move-up') {
|
|
2036
|
-
inboxProps.moveUp();
|
|
2037
|
-
} else if (command === 'move-down') {
|
|
2038
|
-
inboxProps.moveDown();
|
|
2039
|
-
} else if (command === 'activate') {
|
|
2040
|
-
await inboxProps.openThread();
|
|
2041
|
-
} else if (command === 'delete') {
|
|
2042
|
-
if (inboxProps.isViewingThread) {
|
|
2043
|
-
await inboxProps.deleteThread();
|
|
2044
|
-
} else {
|
|
2045
|
-
await inboxProps.deleteSelected();
|
|
2046
|
-
}
|
|
2047
|
-
} else if (command === 'clear') {
|
|
2048
|
-
await inboxProps.clearAll();
|
|
2049
|
-
} else if (command === 'attach' && inboxProps.isViewingThread) {
|
|
2050
|
-
await inboxProps.attachToSession();
|
|
2051
|
-
}
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// View-specific shortcuts
|
|
2056
|
-
if (state.view === 'machines') {
|
|
2057
|
-
const command = resolveMachineListCommand({
|
|
2058
|
-
name: key.name,
|
|
2059
|
-
raw: key.raw,
|
|
2060
|
-
shift: key.shift,
|
|
2061
|
-
});
|
|
2062
|
-
|
|
2063
|
-
if (command === 'move-up') {
|
|
2064
|
-
machineListProps.moveUp();
|
|
2065
|
-
} else if (command === 'move-down') {
|
|
2066
|
-
machineListProps.moveDown();
|
|
2067
|
-
} else if (command === 'activate') {
|
|
2068
|
-
machineListProps.connectSelected();
|
|
2069
|
-
} else if (command === 'refresh') {
|
|
2070
|
-
try {
|
|
2071
|
-
await machineListProps.refresh();
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
flow.showMessage({
|
|
2074
|
-
title: 'Refresh Failed',
|
|
2075
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2076
|
-
variant: 'error',
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
} else if (command === 'help') {
|
|
2080
|
-
flow.showHelp(getDefaultShortcuts());
|
|
2081
|
-
}
|
|
2082
|
-
return;
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
if (state.view === 'projects') {
|
|
2086
|
-
// Panel switching
|
|
2087
|
-
if (key.name === 'tab') {
|
|
2088
|
-
dispatch({ type: 'SWITCH_PANEL' });
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
if (state.panelFocus === 'projects') {
|
|
2093
|
-
if (key.name === 'up' || key.raw === 'k') {
|
|
2094
|
-
projectListProps.moveUp();
|
|
2095
|
-
} else if (key.name === 'down' || key.raw === 'j') {
|
|
2096
|
-
projectListProps.moveDown();
|
|
2097
|
-
} else if (key.name === 'return') {
|
|
2098
|
-
projectListProps.selectProject();
|
|
2099
|
-
} else if (key.raw === 'n') {
|
|
2100
|
-
// In projects panel, 'n' creates new project
|
|
2101
|
-
await handleNewProjectFlow();
|
|
2102
|
-
} else if (key.raw === 'd') {
|
|
2103
|
-
projectListProps.deleteSelected();
|
|
2104
|
-
} else if (key.raw === 'r') {
|
|
2105
|
-
try {
|
|
2106
|
-
await projectListProps.refresh();
|
|
2107
|
-
} catch (error) {
|
|
2108
|
-
flow.showMessage({
|
|
2109
|
-
title: 'Refresh Failed',
|
|
2110
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2111
|
-
variant: 'error',
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
} else {
|
|
2116
|
-
// Workspaces panel
|
|
2117
|
-
const command = resolveSessionBrowserCommand({
|
|
2118
|
-
name: key.name,
|
|
2119
|
-
raw: key.raw,
|
|
2120
|
-
shift: key.shift,
|
|
2121
|
-
});
|
|
2122
|
-
|
|
2123
|
-
if (command === 'move-up') {
|
|
2124
|
-
spacesBrowserProps.moveUp();
|
|
2125
|
-
} else if (command === 'move-down') {
|
|
2126
|
-
spacesBrowserProps.moveDown();
|
|
2127
|
-
} else if (command === 'activate') {
|
|
2128
|
-
// Let the hook handle it:
|
|
2129
|
-
// - workspace: toggle expand/collapse
|
|
2130
|
-
// - session: attach via onAttachSession
|
|
2131
|
-
// - new-session: create via onAttachSession
|
|
2132
|
-
try {
|
|
2133
|
-
await spacesBrowserProps.activateSelected();
|
|
2134
|
-
} catch (error) {
|
|
2135
|
-
flow.showMessage({
|
|
2136
|
-
title: 'Attach Failed',
|
|
2137
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2138
|
-
variant: 'error',
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
} else if (command === 'new') {
|
|
2142
|
-
// In workspaces panel, 'n' always creates new workspace
|
|
2143
|
-
// Sessions are created via expand (Enter) → "+ New session" (Enter)
|
|
2144
|
-
handleNewWorkspaceFlow();
|
|
2145
|
-
} else if (command === 'delete') {
|
|
2146
|
-
// Delete workspace
|
|
2147
|
-
const selected = spacesBrowserProps.selectedItem;
|
|
2148
|
-
if (selected?.type === 'workspace') {
|
|
2149
|
-
const workspace = workspaceInfos.find((item) => item.id === selected.workspace.id);
|
|
2150
|
-
if (workspace) {
|
|
2151
|
-
handleDeleteWorkspace(workspace);
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
} else if (command === 'kill') {
|
|
2155
|
-
// Kill session or stop running process
|
|
2156
|
-
const selected = spacesBrowserProps.selectedItem;
|
|
2157
|
-
if (selected?.type === 'session') {
|
|
2158
|
-
handleDeleteSession(selected.session.id, selected.session.name);
|
|
2159
|
-
} else if (selected?.type === 'process' && selected.status === 'running') {
|
|
2160
|
-
flow.showConfirm({
|
|
2161
|
-
title: 'Stop Process',
|
|
2162
|
-
message: `Stop process "${selected.processName}"?`,
|
|
2163
|
-
variant: 'warning',
|
|
2164
|
-
confirmLabel: 'Stop',
|
|
2165
|
-
onConfirm: () => {
|
|
2166
|
-
void handleStopProcess({
|
|
2167
|
-
workspaceId: selected.workspaceId,
|
|
2168
|
-
processName: selected.processName,
|
|
2169
|
-
});
|
|
2170
|
-
},
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
} else if (command === 'refresh') {
|
|
2174
|
-
try {
|
|
2175
|
-
await spacesBrowserProps.refresh();
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
flow.showMessage({
|
|
2178
|
-
title: 'Refresh Failed',
|
|
2179
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2180
|
-
variant: 'error',
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
} else if (command === 'back') {
|
|
2184
|
-
dispatch({ type: 'SET_PANEL_FOCUS', focus: 'projects' });
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
useEffect(() => {
|
|
2192
|
-
if (!sessionSwitchingRef.current) {
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
if (state.view === 'projects' || state.view === 'machines' || state.view === 'inbox') {
|
|
2197
|
-
sessionSwitchingRef.current = false;
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
if (state.view === 'terminal' && localSessionMode === 'attached') {
|
|
2202
|
-
sessionSwitchingRef.current = false;
|
|
2203
|
-
}
|
|
2204
|
-
}, [localSessionMode, state.view]);
|
|
2205
|
-
|
|
2206
|
-
// Keep local terminal view in sync with backend session lifecycle.
|
|
2207
|
-
useEffect(() => {
|
|
2208
|
-
const action = resolveLocalTerminalSyncAction({
|
|
2209
|
-
isLocalMachineContext,
|
|
2210
|
-
view: state.view,
|
|
2211
|
-
localSessionStatus,
|
|
2212
|
-
localSessionMode,
|
|
2213
|
-
localScriptState,
|
|
2214
|
-
isSessionSwitching: sessionSwitchingRef.current,
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
if (action === 'show-connection-error') {
|
|
2218
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
2219
|
-
dispatch({ type: 'SET_ERROR', error: 'Local session connection failed' });
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
if (action === 'return-to-projects') {
|
|
2224
|
-
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
2225
|
-
void refreshWorkspaces();
|
|
2226
|
-
}
|
|
2227
|
-
}, [
|
|
2228
|
-
isLocalMachineContext,
|
|
2229
|
-
localScriptState,
|
|
2230
|
-
localSessionMode,
|
|
2231
|
-
localSessionStatus,
|
|
2232
|
-
refreshWorkspaces,
|
|
2233
|
-
state.view,
|
|
2234
|
-
]);
|
|
2235
|
-
|
|
2236
|
-
// ========== Render ==========
|
|
2237
|
-
|
|
2238
|
-
// Loading state
|
|
2239
|
-
if (state.isLoading) {
|
|
2240
|
-
return (
|
|
2241
|
-
<Fragment>
|
|
2242
|
-
<Toaster position="top-right" />
|
|
2243
|
-
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center">
|
|
2244
|
-
<text fg={COLORS.loading}>Loading...</text>
|
|
2245
|
-
</box>
|
|
2246
|
-
</Fragment>
|
|
2247
|
-
);
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
// Error state
|
|
2251
|
-
if (state.error) {
|
|
2252
|
-
return (
|
|
2253
|
-
<Fragment>
|
|
2254
|
-
<Toaster position="top-right" />
|
|
2255
|
-
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center">
|
|
2256
|
-
<text fg={COLORS.error}>Error: {state.error}</text>
|
|
2257
|
-
<text fg={COLORS.textDim} marginTop={1}>Press 'q' to quit</text>
|
|
2258
|
-
</box>
|
|
2259
|
-
</Fragment>
|
|
2260
|
-
);
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
// Machine list view (remote mode)
|
|
2264
|
-
if (state.view === 'machines') {
|
|
2265
|
-
return (
|
|
2266
|
-
<Fragment>
|
|
2267
|
-
<Toaster position="top-right" />
|
|
2268
|
-
<box flexDirection="column" flexGrow={1}>
|
|
2269
|
-
<MachineListTUI {...machineListProps} focused={true} />
|
|
2270
|
-
<StatusBar hint="[↑↓] Navigate [Enter] Connect [r] Refresh [?] Help [q] Quit" />
|
|
2271
|
-
<FlowTUI flow={flow} />
|
|
2272
|
-
</box>
|
|
2273
|
-
</Fragment>
|
|
2274
|
-
);
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
// Remote machine view (uses shared remote session engine + backend).
|
|
2278
|
-
if (
|
|
2279
|
-
isRemoteMode &&
|
|
2280
|
-
state.view === 'projects' &&
|
|
2281
|
-
state.selectedMachine &&
|
|
2282
|
-
state.selectedMachine.machineId !== 'local'
|
|
2283
|
-
) {
|
|
2284
|
-
if (!relayConfig || !remoteMachines.identity) {
|
|
2285
|
-
return (
|
|
2286
|
-
<Fragment>
|
|
2287
|
-
<Toaster position="top-right" />
|
|
2288
|
-
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center">
|
|
2289
|
-
<text fg={COLORS.error}>Missing remote identity</text>
|
|
2290
|
-
<text fg={COLORS.textDim} marginTop={1}>Set GITSPACE_IDENTITY_PASSWORD and reconnect</text>
|
|
2291
|
-
</box>
|
|
2292
|
-
</Fragment>
|
|
2293
|
-
);
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
return (
|
|
2297
|
-
<Fragment>
|
|
2298
|
-
<Toaster position="top-right" />
|
|
2299
|
-
<RemoteMachineScreen
|
|
2300
|
-
machine={state.selectedMachine}
|
|
2301
|
-
relayUrl={relayConfig.url}
|
|
2302
|
-
identity={remoteMachines.identity}
|
|
2303
|
-
onBack={() => {
|
|
2304
|
-
dispatch({ type: 'SET_MACHINE', machine: null });
|
|
2305
|
-
dispatch({ type: 'SET_VIEW', view: 'machines' });
|
|
2306
|
-
}}
|
|
2307
|
-
/>
|
|
2308
|
-
</Fragment>
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// Events view
|
|
2313
|
-
if (state.view === 'events') {
|
|
2314
|
-
return (
|
|
2315
|
-
<Fragment>
|
|
2316
|
-
<Toaster position="top-right" />
|
|
2317
|
-
<EventsTui {...eventsProps} />
|
|
2318
|
-
<FlowTUI flow={flow} />
|
|
2319
|
-
<StatusBar hint="[Esc/q] Back [j/k] Navigate" />
|
|
2320
|
-
</Fragment>
|
|
2321
|
-
);
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
// Local terminal view (backend-driven attach lifecycle)
|
|
2325
|
-
if (state.view === 'scripts') {
|
|
2326
|
-
const phase = localScriptState?.phase ?? 'pre';
|
|
2327
|
-
const isRunning = localScriptState?.isRunning ?? true;
|
|
2328
|
-
|
|
2329
|
-
return (
|
|
2330
|
-
<Fragment>
|
|
2331
|
-
<Toaster position="top-right" />
|
|
2332
|
-
<ScriptTerminal
|
|
2333
|
-
ref={scriptTerminalRef}
|
|
2334
|
-
phase={phase}
|
|
2335
|
-
workspaceName={scriptWorkspaceName}
|
|
2336
|
-
isRunning={isRunning}
|
|
2337
|
-
error={localScriptState?.error}
|
|
2338
|
-
exitCode={localScriptState?.exitCode}
|
|
2339
|
-
/>
|
|
2340
|
-
{!isRunning && <FlowTUI flow={flow} />}
|
|
2341
|
-
<StatusBar hint={isRunning ? '[Running scripts...]' : '[Esc/n] Back to workspaces'} />
|
|
2342
|
-
</Fragment>
|
|
2343
|
-
);
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
// Local terminal view (backend-driven attach lifecycle)
|
|
2347
|
-
if (state.view === 'terminal') {
|
|
2348
|
-
const sessionLabel = localAttachedSessionName
|
|
2349
|
-
?? (localScriptState?.isRunning
|
|
2350
|
-
? `Preparing session (${localScriptState.phase})`
|
|
2351
|
-
: 'Connecting session');
|
|
2352
|
-
|
|
2353
|
-
return (
|
|
2354
|
-
<Fragment>
|
|
2355
|
-
<Toaster position="top-right" />
|
|
2356
|
-
<SessionTerminal
|
|
2357
|
-
sessionName={sessionLabel}
|
|
2358
|
-
endpointLabel="local"
|
|
2359
|
-
onData={sendLocalPty}
|
|
2360
|
-
onResize={resizeLocalPty}
|
|
2361
|
-
onDetach={handleTerminalDetach}
|
|
2362
|
-
setWriteCallback={setLocalWriteCallback}
|
|
2363
|
-
interceptShiftTab={!!notifications.activeToast}
|
|
2364
|
-
modalOpen={flow.isOpen}
|
|
2365
|
-
onActivity={handleTerminalActivity}
|
|
2366
|
-
readOnly={isViewOnlySession}
|
|
2367
|
-
/>
|
|
2368
|
-
<FlowTUI flow={flow} />
|
|
2369
|
-
</Fragment>
|
|
2370
|
-
);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
// Inbox view (full-screen)
|
|
2374
|
-
if (state.view === 'inbox') {
|
|
2375
|
-
return (
|
|
2376
|
-
<Fragment>
|
|
2377
|
-
<Toaster position="top-right" />
|
|
2378
|
-
<InboxTUI {...inboxProps} focused={true} />
|
|
2379
|
-
</Fragment>
|
|
2380
|
-
);
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
// Main project/workspace view
|
|
2384
|
-
return (
|
|
2385
|
-
<Fragment>
|
|
2386
|
-
<Toaster position="top-right" />
|
|
2387
|
-
<box flexDirection="column" flexGrow={1} width="100%">
|
|
2388
|
-
{/* ASCII Art Header */}
|
|
2389
|
-
<box flexDirection="row" width="100%" height={13}>
|
|
2390
|
-
{/* ASCII art on left - fixed width */}
|
|
2391
|
-
<box flexDirection="column" alignItems="flex-start" paddingLeft={1} width={68}>
|
|
2392
|
-
{ASCII_LINES.map((line, i) => (
|
|
2393
|
-
<text key={i} fg={line.color}>{line.text}</text>
|
|
2394
|
-
))}
|
|
2395
|
-
</box>
|
|
2396
|
-
|
|
2397
|
-
{/* Status & Notifications on right */}
|
|
2398
|
-
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingTop={1}>
|
|
2399
|
-
{/* Daemon status line */}
|
|
2400
|
-
<box flexDirection="row" gap={2}>
|
|
2401
|
-
<text fg={daemonStatus.tmux.running ? COLORS.title : COLORS.textDim}>
|
|
2402
|
-
tmux: {daemonStatus.tmux.running ? '●' : '○'} {daemonStatus.tmux.sessions ?? 0} sessions
|
|
2403
|
-
</text>
|
|
2404
|
-
<text fg={daemonStatus.serve.running ? COLORS.title : COLORS.textDim}>
|
|
2405
|
-
relay: {formatRelayStatus(daemonStatus.serve.relayStatus)} {daemonStatus.serve.running ? (daemonStatus.serve.clients ?? 0) + ' clients' : 'off'}
|
|
2406
|
-
</text>
|
|
2407
|
-
</box>
|
|
2408
|
-
|
|
2409
|
-
{/* Uptime info */}
|
|
2410
|
-
{(daemonStatus.tmux.running || daemonStatus.serve.running) && (
|
|
2411
|
-
<text fg={COLORS.textDim}>
|
|
2412
|
-
{daemonStatus.tmux.uptime ? `tmux: ${formatUptime(daemonStatus.tmux.uptime)}` : ''}
|
|
2413
|
-
{daemonStatus.tmux.uptime && daemonStatus.serve.uptime ? ' ' : ''}
|
|
2414
|
-
{daemonStatus.serve.uptime ? `serve: ${formatUptime(daemonStatus.serve.uptime)}` : ''}
|
|
2415
|
-
</text>
|
|
2416
|
-
)}
|
|
2417
|
-
|
|
2418
|
-
{/* Version mismatch warning */}
|
|
2419
|
-
{daemonStatus.versionMismatch && (
|
|
2420
|
-
<text fg={COLORS.error}>⚠ Version mismatch - restart daemons</text>
|
|
2421
|
-
)}
|
|
2422
|
-
|
|
2423
|
-
{/* Notifications */}
|
|
2424
|
-
<box marginTop={1}>
|
|
2425
|
-
{inboxUnreadCount > 0 ? (
|
|
2426
|
-
<box flexDirection="column">
|
|
2427
|
-
<text fg={COLORS.loading}>{'📥'} {inboxUnreadCount} notification{inboxUnreadCount > 1 ? 's' : ''}</text>
|
|
2428
|
-
<text fg={COLORS.textDim}>[i] view inbox</text>
|
|
2429
|
-
</box>
|
|
2430
|
-
) : (
|
|
2431
|
-
<text fg={COLORS.textDim}>No notifications</text>
|
|
2432
|
-
)}
|
|
2433
|
-
</box>
|
|
2434
|
-
</box>
|
|
2435
|
-
</box>
|
|
2436
|
-
|
|
2437
|
-
{/* Main content - two panel layout */}
|
|
2438
|
-
<box flexDirection="row" flexGrow={1} width="100%" gap={1} paddingLeft={1} paddingRight={1}>
|
|
2439
|
-
<ProjectListTUI {...projectListProps} focused={state.panelFocus === 'projects'} />
|
|
2440
|
-
<SpacesBrowserTUI {...spacesBrowserProps} focused={state.panelFocus === 'workspaces'} />
|
|
2441
|
-
</box>
|
|
2442
|
-
|
|
2443
|
-
{/* Status bar */}
|
|
2444
|
-
<StatusBar
|
|
2445
|
-
hint={state.panelFocus === 'projects'
|
|
2446
|
-
? '[Tab] Switch [Enter] Select [n] New Project [d] Delete [,] Settings [?] Help [q] Quit'
|
|
2447
|
-
: getWorkspacesPanelHint(spacesBrowserProps.selectedItem)
|
|
2448
|
-
}
|
|
2449
|
-
/>
|
|
2450
|
-
|
|
2451
|
-
{/* Flow modal overlay */}
|
|
2452
|
-
<FlowTUI flow={flow} />
|
|
2453
|
-
|
|
2454
|
-
{/* Workspace creation flow modal */}
|
|
2455
|
-
<WorkspaceFlowModal flow={workspaceFlow} />
|
|
2456
|
-
|
|
2457
|
-
{/* Project creation flow modal */}
|
|
2458
|
-
<ProjectFlowModal flow={projectFlow} />
|
|
2459
|
-
|
|
2460
|
-
{/* Settings flow modal */}
|
|
2461
|
-
<SettingsFlowModal flow={settingsFlow} />
|
|
2462
|
-
</box>
|
|
2463
|
-
</Fragment>
|
|
2464
|
-
);
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// ============================================================================
|
|
2468
|
-
// Workspace Flow Modal Component
|
|
2469
|
-
// ============================================================================
|
|
2470
|
-
|
|
2471
|
-
function WorkspaceFlowModal({ flow }: { flow: WorkspaceFlowState }) {
|
|
2472
|
-
if (flow.type === 'closed') {
|
|
2473
|
-
return null;
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
const modalWidth = 60;
|
|
2477
|
-
// Calculate modal height based on content:
|
|
2478
|
-
// - source-select: title + spacer + (options * 2 lines each) + (spacers between) + spacer + hint + border/padding
|
|
2479
|
-
// - branch/linear-select: title + items (scrollable) + hint + border/padding
|
|
2480
|
-
// - manual-name-input: title + label + input box + error? + hint + border/padding
|
|
2481
|
-
// - manual-branch-input: title + label + input box + workspace display + error? + hint + border/padding
|
|
2482
|
-
const modalHeight = flow.type === 'manual-name-input' ? 10 :
|
|
2483
|
-
flow.type === 'manual-branch-input' ? 13 :
|
|
2484
|
-
flow.type === 'loading' || flow.type === 'creating' ? 6 :
|
|
2485
|
-
flow.type === 'source-select' ? 6 + flow.options.length * 3 :
|
|
2486
|
-
flow.type === 'branch-select' ? Math.min(16, 6 + flow.branches.length) :
|
|
2487
|
-
flow.type === 'linear-select' ? Math.min(16, 6 + flow.issues.length) : 10;
|
|
2488
|
-
|
|
2489
|
-
return (
|
|
2490
|
-
<box
|
|
2491
|
-
position="absolute"
|
|
2492
|
-
width="100%"
|
|
2493
|
-
height="100%"
|
|
2494
|
-
justifyContent="center"
|
|
2495
|
-
alignItems="center"
|
|
2496
|
-
>
|
|
2497
|
-
<box
|
|
2498
|
-
flexDirection="column"
|
|
2499
|
-
width={modalWidth}
|
|
2500
|
-
height={modalHeight}
|
|
2501
|
-
borderStyle="rounded"
|
|
2502
|
-
borderColor={COLORS.borderFocused}
|
|
2503
|
-
backgroundColor="#1a1a2e"
|
|
2504
|
-
padding={1}
|
|
2505
|
-
>
|
|
2506
|
-
{/* Loading state */}
|
|
2507
|
-
{flow.type === 'loading' && (
|
|
2508
|
-
<>
|
|
2509
|
-
<text fg={COLORS.title} height={1}>{flow.title}</text>
|
|
2510
|
-
<text fg={COLORS.loading} height={1} marginTop={1}>{flow.message}</text>
|
|
2511
|
-
</>
|
|
2512
|
-
)}
|
|
2513
|
-
|
|
2514
|
-
{/* Creating state */}
|
|
2515
|
-
{flow.type === 'creating' && (
|
|
2516
|
-
<>
|
|
2517
|
-
<text fg={COLORS.title} height={1}>Creating Workspace</text>
|
|
2518
|
-
<text fg={COLORS.loading} height={1} marginTop={1}>{flow.message ?? `Creating ${flow.workspaceName}...`}</text>
|
|
2519
|
-
</>
|
|
2520
|
-
)}
|
|
2521
|
-
|
|
2522
|
-
{/* Source selection */}
|
|
2523
|
-
{flow.type === 'source-select' && (
|
|
2524
|
-
<>
|
|
2525
|
-
<text fg={COLORS.title} height={1}>Create Workspace From</text>
|
|
2526
|
-
<box height={1} />
|
|
2527
|
-
{flow.options.flatMap((opt, i) => [
|
|
2528
|
-
<text key={`${opt.value}-label`} fg={i === flow.selectedIndex ? COLORS.selected : COLORS.text} height={1}>
|
|
2529
|
-
{i === flow.selectedIndex ? '▸ ' : ' '}{opt.label}
|
|
2530
|
-
</text>,
|
|
2531
|
-
<text key={`${opt.value}-desc`} fg={COLORS.textDim} height={1} paddingLeft={4}>{opt.description}</text>,
|
|
2532
|
-
i < flow.options.length - 1 ? <box key={`${opt.value}-spacer`} height={1} /> : null,
|
|
2533
|
-
].filter(Boolean))}
|
|
2534
|
-
<box height={1} />
|
|
2535
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Select [Esc] Cancel</text>
|
|
2536
|
-
</>
|
|
2537
|
-
)}
|
|
2538
|
-
|
|
2539
|
-
{/* Branch selection */}
|
|
2540
|
-
{flow.type === 'branch-select' && (
|
|
2541
|
-
<>
|
|
2542
|
-
<text fg={COLORS.title} height={1}>Select Branch</text>
|
|
2543
|
-
<box flexDirection="column" marginTop={1} flexGrow={1} overflow="hidden">
|
|
2544
|
-
{flow.branches.slice(
|
|
2545
|
-
Math.max(0, flow.selectedIndex - 5),
|
|
2546
|
-
Math.max(0, flow.selectedIndex - 5) + 10
|
|
2547
|
-
).map((branch, i) => {
|
|
2548
|
-
const actualIndex = Math.max(0, flow.selectedIndex - 5) + i;
|
|
2549
|
-
return (
|
|
2550
|
-
<text key={branch} height={1} fg={actualIndex === flow.selectedIndex ? COLORS.selected : COLORS.text}>
|
|
2551
|
-
{actualIndex === flow.selectedIndex ? '▸ ' : ' '}{branch}
|
|
2552
|
-
</text>
|
|
2553
|
-
);
|
|
2554
|
-
})}
|
|
2555
|
-
</box>
|
|
2556
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Select [Esc] Cancel</text>
|
|
2557
|
-
</>
|
|
2558
|
-
)}
|
|
2559
|
-
|
|
2560
|
-
{/* Linear issue selection */}
|
|
2561
|
-
{flow.type === 'linear-select' && (
|
|
2562
|
-
<>
|
|
2563
|
-
<text fg={COLORS.title} height={1}>Select Linear Issue</text>
|
|
2564
|
-
<box flexDirection="column" marginTop={1} flexGrow={1} overflow="hidden">
|
|
2565
|
-
{flow.issues.slice(
|
|
2566
|
-
Math.max(0, flow.selectedIndex - 5),
|
|
2567
|
-
Math.max(0, flow.selectedIndex - 5) + 10
|
|
2568
|
-
).map((issue, i) => {
|
|
2569
|
-
const actualIndex = Math.max(0, flow.selectedIndex - 5) + i;
|
|
2570
|
-
const label = `${issue.identifier} - ${issue.title.slice(0, 40)}${issue.title.length > 40 ? '...' : ''}`;
|
|
2571
|
-
return (
|
|
2572
|
-
<text key={issue.id} height={1} fg={actualIndex === flow.selectedIndex ? COLORS.selected : COLORS.text}>
|
|
2573
|
-
{actualIndex === flow.selectedIndex ? '▸ ' : ' '}{label}
|
|
2574
|
-
</text>
|
|
2575
|
-
);
|
|
2576
|
-
})}
|
|
2577
|
-
</box>
|
|
2578
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Select [Esc] Cancel</text>
|
|
2579
|
-
</>
|
|
2580
|
-
)}
|
|
2581
|
-
|
|
2582
|
-
{/* Manual workspace name input */}
|
|
2583
|
-
{flow.type === 'manual-name-input' && (
|
|
2584
|
-
<>
|
|
2585
|
-
<text fg={COLORS.title} height={1}>New Workspace (1/2)</text>
|
|
2586
|
-
<text fg={COLORS.text} height={1} marginTop={1}>Enter workspace name:</text>
|
|
2587
|
-
<box
|
|
2588
|
-
marginTop={1}
|
|
2589
|
-
borderStyle="rounded"
|
|
2590
|
-
borderColor={COLORS.border}
|
|
2591
|
-
padding={0}
|
|
2592
|
-
width="100%"
|
|
2593
|
-
>
|
|
2594
|
-
<text fg={COLORS.text} height={1}>{flow.inputValue || ' '}_</text>
|
|
2595
|
-
</box>
|
|
2596
|
-
{flow.error && <text fg={COLORS.error} height={1} marginTop={1}>{flow.error}</text>}
|
|
2597
|
-
<text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Next [Esc] Cancel</text>
|
|
2598
|
-
</>
|
|
2599
|
-
)}
|
|
2600
|
-
|
|
2601
|
-
{/* Manual branch name input */}
|
|
2602
|
-
{flow.type === 'manual-branch-input' && (
|
|
2603
|
-
<>
|
|
2604
|
-
<text fg={COLORS.title} height={1}>New Workspace (2/2)</text>
|
|
2605
|
-
<text fg={COLORS.text} height={1} marginTop={1}>Enter branch name (slashes allowed):</text>
|
|
2606
|
-
<box
|
|
2607
|
-
marginTop={1}
|
|
2608
|
-
borderStyle="rounded"
|
|
2609
|
-
borderColor={COLORS.border}
|
|
2610
|
-
padding={0}
|
|
2611
|
-
width="100%"
|
|
2612
|
-
>
|
|
2613
|
-
<text fg={COLORS.text} height={1}>{flow.inputValue || ' '}_</text>
|
|
2614
|
-
</box>
|
|
2615
|
-
<text fg={COLORS.textDim} height={1} marginTop={1}>Workspace: {flow.workspaceName}</text>
|
|
2616
|
-
{flow.error && <text fg={COLORS.error} height={1} marginTop={1}>{flow.error}</text>}
|
|
2617
|
-
<text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Create [Esc] Cancel</text>
|
|
2618
|
-
</>
|
|
2619
|
-
)}
|
|
2620
|
-
</box>
|
|
2621
|
-
</box>
|
|
2622
|
-
);
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
// ============================================================================
|
|
2626
|
-
// Project Flow Modal Component
|
|
2627
|
-
// ============================================================================
|
|
2628
|
-
|
|
2629
|
-
function ProjectFlowModal({ flow }: { flow: ProjectFlowState }) {
|
|
2630
|
-
if (flow.type === 'closed') {
|
|
2631
|
-
return null;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
const modalWidth = 70;
|
|
2635
|
-
const modalHeight = flow.type === 'repo-select' ? 18 :
|
|
2636
|
-
flow.type === 'onboarding' ? 14 :
|
|
2637
|
-
8;
|
|
2638
|
-
|
|
2639
|
-
return (
|
|
2640
|
-
<box
|
|
2641
|
-
position="absolute"
|
|
2642
|
-
width="100%"
|
|
2643
|
-
height="100%"
|
|
2644
|
-
justifyContent="center"
|
|
2645
|
-
alignItems="center"
|
|
2646
|
-
>
|
|
2647
|
-
<box
|
|
2648
|
-
flexDirection="column"
|
|
2649
|
-
width={modalWidth}
|
|
2650
|
-
height={modalHeight}
|
|
2651
|
-
borderStyle="rounded"
|
|
2652
|
-
borderColor={COLORS.borderFocused}
|
|
2653
|
-
backgroundColor="#1a1a2e"
|
|
2654
|
-
padding={1}
|
|
2655
|
-
>
|
|
2656
|
-
{/* Loading repos state */}
|
|
2657
|
-
{flow.type === 'loading-repos' && (
|
|
2658
|
-
<>
|
|
2659
|
-
<text fg={COLORS.title} height={1}>New Project</text>
|
|
2660
|
-
<text fg={COLORS.loading} height={1} marginTop={1}>Fetching repositories...</text>
|
|
2661
|
-
</>
|
|
2662
|
-
)}
|
|
2663
|
-
|
|
2664
|
-
{/* Repository selection */}
|
|
2665
|
-
{flow.type === 'repo-select' && (
|
|
2666
|
-
<>
|
|
2667
|
-
<text fg={COLORS.title} height={1}>Select Repository</text>
|
|
2668
|
-
<box flexDirection="column" marginTop={1} flexGrow={1} overflow="hidden">
|
|
2669
|
-
{flow.repos.slice(
|
|
2670
|
-
Math.max(0, flow.selectedIndex - 5),
|
|
2671
|
-
Math.max(0, flow.selectedIndex - 5) + 10
|
|
2672
|
-
).map((repo, i) => {
|
|
2673
|
-
const actualIndex = Math.max(0, flow.selectedIndex - 5) + i;
|
|
2674
|
-
return (
|
|
2675
|
-
<text key={repo} height={1} fg={actualIndex === flow.selectedIndex ? COLORS.selected : COLORS.text}>
|
|
2676
|
-
{actualIndex === flow.selectedIndex ? '▸ ' : ' '}{repo}
|
|
2677
|
-
</text>
|
|
2678
|
-
);
|
|
2679
|
-
})}
|
|
2680
|
-
</box>
|
|
2681
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Select [Esc] Cancel</text>
|
|
2682
|
-
</>
|
|
2683
|
-
)}
|
|
2684
|
-
|
|
2685
|
-
{/* Cloning state */}
|
|
2686
|
-
{flow.type === 'cloning' && (
|
|
2687
|
-
<>
|
|
2688
|
-
<text fg={COLORS.title} height={1}>Cloning Repository</text>
|
|
2689
|
-
<text fg={COLORS.loading} height={1} marginTop={1}>Cloning {flow.repo}...</text>
|
|
2690
|
-
</>
|
|
2691
|
-
)}
|
|
2692
|
-
|
|
2693
|
-
{/* Onboarding steps */}
|
|
2694
|
-
{flow.type === 'onboarding' && (
|
|
2695
|
-
<ProjectOnboardingStepTUI flow={flow} colors={COLORS} />
|
|
2696
|
-
)}
|
|
2697
|
-
|
|
2698
|
-
{/* Creating state */}
|
|
2699
|
-
{flow.type === 'creating' && (
|
|
2700
|
-
<>
|
|
2701
|
-
<text fg={COLORS.title} height={1}>Creating Project</text>
|
|
2702
|
-
<text fg={COLORS.loading} height={1} marginTop={1}>Setting up {flow.projectName}...</text>
|
|
2703
|
-
</>
|
|
2704
|
-
)}
|
|
2705
|
-
</box>
|
|
2706
|
-
</box>
|
|
2707
|
-
);
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
// ============================================================================
|
|
2711
|
-
// Settings Flow Modal Component
|
|
2712
|
-
// ============================================================================
|
|
2713
|
-
|
|
2714
|
-
function SettingsFlowModal({ flow }: { flow: SettingsFlowState }) {
|
|
2715
|
-
if (flow.type === 'closed') {
|
|
2716
|
-
return null;
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
const modalWidth = 50;
|
|
2720
|
-
const modalHeight = flow.type === 'main-menu' ? 14 :
|
|
2721
|
-
flow.type === 'types-menu' ? 12 :
|
|
2722
|
-
8;
|
|
2723
|
-
|
|
2724
|
-
const typeLabels: Record<keyof NotificationTypeConfig, string> = {
|
|
2725
|
-
exit: 'Exit (process completion)',
|
|
2726
|
-
idle: 'Idle (terminal idle)',
|
|
2727
|
-
bell: 'Bell (terminal bell)',
|
|
2728
|
-
title: 'Title (title change)',
|
|
2729
|
-
osc: 'OSC (escape sequences)',
|
|
2730
|
-
};
|
|
2731
|
-
|
|
2732
|
-
return (
|
|
2733
|
-
<box
|
|
2734
|
-
position="absolute"
|
|
2735
|
-
width="100%"
|
|
2736
|
-
height="100%"
|
|
2737
|
-
justifyContent="center"
|
|
2738
|
-
alignItems="center"
|
|
2739
|
-
>
|
|
2740
|
-
<box
|
|
2741
|
-
flexDirection="column"
|
|
2742
|
-
width={modalWidth}
|
|
2743
|
-
height={modalHeight}
|
|
2744
|
-
borderStyle="rounded"
|
|
2745
|
-
borderColor={COLORS.borderFocused}
|
|
2746
|
-
backgroundColor="#1a1a2e"
|
|
2747
|
-
padding={1}
|
|
2748
|
-
>
|
|
2749
|
-
{/* Main menu */}
|
|
2750
|
-
{flow.type === 'main-menu' && (
|
|
2751
|
-
<>
|
|
2752
|
-
<text fg={COLORS.title} height={1}>Notification Settings</text>
|
|
2753
|
-
<box height={1} />
|
|
2754
|
-
<text fg={flow.selectedIndex === 0 ? COLORS.selected : COLORS.text} height={1}>
|
|
2755
|
-
{flow.selectedIndex === 0 ? '▸ ' : ' '}{flow.config.enabled ? '✓' : '✗'} Notifications Enabled
|
|
2756
|
-
</text>
|
|
2757
|
-
<text fg={flow.selectedIndex === 1 ? COLORS.selected : COLORS.text} height={1}>
|
|
2758
|
-
{flow.selectedIndex === 1 ? '▸ ' : ' '}{flow.config.toast.enabled ? '✓' : '✗'} Toast Notifications
|
|
2759
|
-
</text>
|
|
2760
|
-
<text fg={flow.selectedIndex === 2 ? COLORS.selected : COLORS.text} height={1}>
|
|
2761
|
-
{flow.selectedIndex === 2 ? '▸ ' : ' '}Min Command Duration: {Math.round(flow.config.minCommandDurationMs / 1000)}s
|
|
2762
|
-
</text>
|
|
2763
|
-
<text fg={flow.selectedIndex === 3 ? COLORS.selected : COLORS.text} height={1}>
|
|
2764
|
-
{flow.selectedIndex === 3 ? '▸ ' : ' '}Toast Hold Duration: {Math.round(flow.config.toast.holdWhenIdleMs / 1000)}s
|
|
2765
|
-
</text>
|
|
2766
|
-
<text fg={flow.selectedIndex === 4 ? COLORS.selected : COLORS.text} height={1}>
|
|
2767
|
-
{flow.selectedIndex === 4 ? '▸ ' : ' '}Notification Types...
|
|
2768
|
-
</text>
|
|
2769
|
-
<text fg={flow.selectedIndex === 5 ? COLORS.selected : COLORS.text} height={1}>
|
|
2770
|
-
{flow.selectedIndex === 5 ? '▸ ' : ' '}↺ Reset to Defaults
|
|
2771
|
-
</text>
|
|
2772
|
-
<box height={1} />
|
|
2773
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Toggle/Select [Esc] Close</text>
|
|
2774
|
-
</>
|
|
2775
|
-
)}
|
|
2776
|
-
|
|
2777
|
-
{/* Types submenu */}
|
|
2778
|
-
{flow.type === 'types-menu' && (
|
|
2779
|
-
<>
|
|
2780
|
-
<text fg={COLORS.title} height={1}>Notification Types</text>
|
|
2781
|
-
<box height={1} />
|
|
2782
|
-
{(Object.keys(typeLabels) as Array<keyof NotificationTypeConfig>).map((key, i) => (
|
|
2783
|
-
<text key={key} fg={flow.selectedIndex === i ? COLORS.selected : COLORS.text} height={1}>
|
|
2784
|
-
{flow.selectedIndex === i ? '▸ ' : ' '}{flow.config.types[key] ? '✓' : '✗'} {typeLabels[key]}
|
|
2785
|
-
</text>
|
|
2786
|
-
))}
|
|
2787
|
-
<text fg={flow.selectedIndex === 5 ? COLORS.selected : COLORS.text} height={1}>
|
|
2788
|
-
{flow.selectedIndex === 5 ? '▸ ' : ' '}← Back
|
|
2789
|
-
</text>
|
|
2790
|
-
<box height={1} />
|
|
2791
|
-
<text fg={COLORS.textDim} height={1}>[↑↓] Navigate [Enter] Toggle [Esc] Back</text>
|
|
2792
|
-
</>
|
|
2793
|
-
)}
|
|
2794
|
-
|
|
2795
|
-
{/* Edit duration */}
|
|
2796
|
-
{flow.type === 'edit-duration' && (
|
|
2797
|
-
<>
|
|
2798
|
-
<text fg={COLORS.title} height={1}>Min Command Duration</text>
|
|
2799
|
-
<text fg={COLORS.text} height={1} marginTop={1}>Duration in seconds before exit notification:</text>
|
|
2800
|
-
<box
|
|
2801
|
-
marginTop={1}
|
|
2802
|
-
borderStyle="rounded"
|
|
2803
|
-
borderColor={COLORS.border}
|
|
2804
|
-
padding={0}
|
|
2805
|
-
width="100%"
|
|
2806
|
-
>
|
|
2807
|
-
<text fg={COLORS.text} height={1}>{flow.value || '0'}_</text>
|
|
2808
|
-
</box>
|
|
2809
|
-
<text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Save [Esc] Cancel</text>
|
|
2810
|
-
</>
|
|
2811
|
-
)}
|
|
2812
|
-
|
|
2813
|
-
{/* Edit hold duration */}
|
|
2814
|
-
{flow.type === 'edit-hold-duration' && (
|
|
2815
|
-
<>
|
|
2816
|
-
<text fg={COLORS.title} height={1}>Toast Hold Duration</text>
|
|
2817
|
-
<text fg={COLORS.text} height={1} marginTop={1}>Hold toasts when idle (seconds, 0 to disable):</text>
|
|
2818
|
-
<box
|
|
2819
|
-
marginTop={1}
|
|
2820
|
-
borderStyle="rounded"
|
|
2821
|
-
borderColor={COLORS.border}
|
|
2822
|
-
padding={0}
|
|
2823
|
-
width="100%"
|
|
2824
|
-
>
|
|
2825
|
-
<text fg={COLORS.text} height={1}>{flow.value || '0'}_</text>
|
|
2826
|
-
</box>
|
|
2827
|
-
<text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Save [Esc] Cancel</text>
|
|
2828
|
-
</>
|
|
2829
|
-
)}
|
|
2830
|
-
</box>
|
|
2831
|
-
</box>
|
|
2832
|
-
);
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
// ============================================================================
|
|
2836
|
-
// Status Bar Helpers
|
|
2837
|
-
// ============================================================================
|
|
2838
|
-
|
|
2839
|
-
function getWorkspacesPanelHint(selectedItem: TreeItem | null | undefined): string {
|
|
2840
|
-
if (selectedItem?.type === 'session') {
|
|
2841
|
-
return '[Tab] Switch [Enter] Attach [x] Kill [n] New Workspace [d] Delete [,] Settings [?] Help [q] Quit';
|
|
2842
|
-
}
|
|
2843
|
-
if (selectedItem?.type === 'process') {
|
|
2844
|
-
if (selectedItem.status === 'running') {
|
|
2845
|
-
return '[Tab] Switch [Enter] View [x] Stop [,] Settings [?] Help [q] Quit';
|
|
2846
|
-
}
|
|
2847
|
-
return '[Tab] Switch [Enter] Start [,] Settings [?] Help [q] Quit';
|
|
2848
|
-
}
|
|
2849
|
-
if (selectedItem?.type === 'workspace') {
|
|
2850
|
-
return '[Tab] Switch [Enter] Expand [n] New Workspace [d] Delete [,] Settings [?] Help [q] Quit';
|
|
2851
|
-
}
|
|
2852
|
-
if (selectedItem?.type === 'process-disabled') {
|
|
2853
|
-
return '[Tab] Switch [Enter] Disabled [,] Settings [?] Help [q] Quit';
|
|
2854
|
-
}
|
|
2855
|
-
if (selectedItem?.type === 'process-config-error') {
|
|
2856
|
-
return '[Tab] Switch [Enter] Fix Process Config [,] Settings [?] Help [q] Quit';
|
|
2857
|
-
}
|
|
2858
|
-
if (selectedItem?.type === 'edit-processes') {
|
|
2859
|
-
return '[Tab] Switch [Enter] Edit Processes Config [,] Settings [?] Help [q] Quit';
|
|
2860
|
-
}
|
|
2861
|
-
if (selectedItem?.type === 'events') {
|
|
2862
|
-
return '[Tab] Switch [Enter] Open Events [,] Settings [?] Help [q] Quit';
|
|
2863
|
-
}
|
|
2864
|
-
if (selectedItem?.type === 'new-session') {
|
|
2865
|
-
return '[Tab] Switch [Enter] New Session [,] Settings [?] Help [q] Quit';
|
|
2866
|
-
}
|
|
2867
|
-
return '[Tab] Switch [Enter] Open [n] New Workspace [,] Settings [?] Help [q] Quit';
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
// ============================================================================
|
|
2871
|
-
// Status Bar Component
|
|
2872
|
-
// ============================================================================
|
|
2873
|
-
|
|
2874
|
-
function StatusBar({ hint }: { hint: string }) {
|
|
2875
|
-
return (
|
|
2876
|
-
<box width="100%" height={1} backgroundColor={COLORS.statusBar}>
|
|
2877
|
-
<text fg={COLORS.textDim} paddingLeft={1}>{hint}</text>
|
|
2878
|
-
</box>
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
// ============================================================================
|
|
2883
|
-
// Entry Point
|
|
2884
|
-
// ============================================================================
|
|
2885
|
-
|
|
2886
|
-
/** @deprecated Use RelayConfig instead */
|
|
2887
|
-
export type TUIRelayConfig = RelayConfig;
|
|
2888
|
-
|
|
2889
|
-
export async function launchTUI(
|
|
2890
|
-
relayConfig?: RelayConfig,
|
|
2891
|
-
options: { ignoreKeychainAndSkipSecrets?: boolean } = {}
|
|
2892
|
-
): Promise<void> {
|
|
2893
|
-
await initializeSecretRuntime({
|
|
2894
|
-
ignoreKeychainAndSkipSecrets: options.ignoreKeychainAndSkipSecrets,
|
|
2895
|
-
});
|
|
2896
|
-
|
|
2897
|
-
const renderer = await createCliRenderer({
|
|
2898
|
-
exitOnCtrlC: false,
|
|
2899
|
-
targetFps: 30,
|
|
2900
|
-
useMouse: true,
|
|
2901
|
-
});
|
|
2902
|
-
const root = createRoot(renderer);
|
|
2903
|
-
|
|
2904
|
-
// Clean exit handler
|
|
2905
|
-
const handleQuit = () => {
|
|
2906
|
-
renderer.destroy();
|
|
2907
|
-
|
|
2908
|
-
const legacyReminder = consumeLegacyCleanupReminderForTui();
|
|
2909
|
-
if (legacyReminder) {
|
|
2910
|
-
logger.warning(legacyReminder);
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
process.exit(0);
|
|
2914
|
-
};
|
|
2915
|
-
|
|
2916
|
-
// Handle SIGINT
|
|
2917
|
-
process.on('SIGINT', handleQuit);
|
|
2918
|
-
|
|
2919
|
-
// Cleanup on exit
|
|
2920
|
-
process.on('exit', () => {
|
|
2921
|
-
// Reset terminal state
|
|
2922
|
-
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
2923
|
-
process.stdout.write('\x1b[?1049l'); // Exit alternate screen
|
|
2924
|
-
process.stdout.write('\x1b[0m'); // Reset colors
|
|
2925
|
-
});
|
|
2926
|
-
|
|
2927
|
-
root.render(<App relayConfig={relayConfig} onQuit={handleQuit} />);
|
|
2928
|
-
renderer.start();
|
|
2929
|
-
}
|