feat: replace terminal launchers with embedded ttyd terminals#156
Merged
feat: replace terminal launchers with embedded ttyd terminals#156
Conversation
Adds two new nullable columns to store the port and PID of the ttyd child process that serves each Claude Code terminal session. Bumps SCHEMA_VERSION to 11 and updates the fresh-install schema, DeploymentRow type, rowToDeployment mapper, Deployment type, and adds updateTtydInfo() to deployments.ts.
Implements the full ttyd lifecycle: verify installation, spawn/kill processes, allocate ports from 7700-7799 range, and reconcile orphaned deployments on startup. Includes comprehensive unit tests (16 passing).
Delete the terminal.ts abstraction and all adapters (Ghostty, iTerm2, macOS Terminal) along with their tests. Remove the three terminal_* SettingKey entries from types and defaults. Export the new ttyd process manager from index.ts in place of the old terminal exports. Remaining errors in launch.ts are expected — will be rewired in Task 4.
Full-viewport panel that slides in from the right, renders the ttyd process in an iframe, and exposes End Session with Escape-to-close.
Remove terminal_app, terminal_window_title, and terminal_tab_title_pattern from DB seed fixtures in all 10 E2E specs — these keys are no longer valid SettingKey values and should not be seeded into test databases.
…eedback - isTtydAlive: handle EPERM (process alive, different user) and re-throw unexpected errors - verifyTtyd: distinguish "not installed" (ENOENT/exit 1) from other failures - spawnTtyd: add post-spawn health check with 300ms delay, now async - getDb: wrap reconcileOrphanedDeployments in try-catch so startup never crashes - endSession: wrap killTtyd in try-catch so DB update always proceeds - TerminalPanel: surface endSession errors in the UI header
The FilterEdgeSwipe was hidden on desktop via a media query from when launch meant opening a Ghostty window. Now that the terminal is embedded in the dashboard, the action sheet needs to be accessible on all viewports.
The FilterEdgeSwipe (swipe-up sheet) is mobile-only. Desktop now gets a visible button bar at the bottom of the issue detail page with "Launch with Claude", "Re-assign", and "Close issue" buttons. The launch button hides when a session is active.
- Move IssueActionSheet above body text so desktop buttons appear after metadata, not at the bottom - Center the desktop action bar with justify-content: center - Add frame-src CSP directive for ttyd ports 7700-7799 so the terminal iframe is not blocked by the browser
The iframe was conditionally rendered with {open && <iframe>}, which
unmounted it when the panel closed. This disconnected the WebSocket
and ttyd's -q flag killed the process. Now the iframe stays mounted
(hidden behind translateX(100%)) so the connection persists across
panel open/close cycles.
Once ttyd has spawned and the deployment is active, redirect to the issue detail page where the Open Terminal button is ready. The old progress page was designed for fire-and-forget Ghostty launches — with embedded ttyd terminals there's no reason to leave the user staring at a spinner.
- Desktop: replace left-edge swipe handle with visible X button in header + clickable backdrop to close the panel - Mobile: keep the swipe handle (hidden on desktop via media query) - Close issue now ends the active terminal session before closing the GitHub issue, preventing orphaned ttyd processes
Previously the return value of endSession() was silently discarded, meaning kill failures went unnoticed. Now the result is inspected and a warning toast is shown if the session could not be stopped cleanly.
Four tests covering the endSession server action: kill succeeds before DB update, kill failure is non-fatal (DB update still runs), no kill when ttydPid is null, and graceful handling when the deployment row is missing.
Cover the try/catch at steps 9-10: verify that pending deployment rows are deleted when allocatePort or spawnTtyd throws, that the slot is freed for a subsequent launch, and that the original error is propagated even when the rollback itself fails.
Replace all Ghostty mentions in source files with ttyd or generic equivalents — JSDoc examples, user-facing placeholder copy, e2e skip guards, and inline comments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed
Removed:
TerminalLauncherinterface, Ghostty/iTerm2/Terminal.app implementations,terminal_app/terminal_window_title/terminal_tab_title_patternsettingsAdded:
packages/core/src/launch/ttyd.ts— ttyd process manager (verify, spawn, kill, health check, port allocation, orphan reconciliation)packages/web/components/terminal/— TerminalPanel slide-out + OpenTerminalButton with health pollingttyd_portandttyd_pidcolumns on deploymentsframe-srcfor ttyd iframe originscheckTtydAliveserver action for frontend liveness pollingSecurity: Shell command escaping,
; exitprevents raw shell exposure after Claude exits,-qflag exits ttyd on disconnect,buildClaudeCommandmetachar validation preservedTest plan
pnpm turbo typecheck— 0 errors across all packagespnpm --filter @issuectl/core test— 354 tests passing