Show close UX for terminal tabs and split panes; ⌘W ends the session (#254) - #262
Merged
Conversation
…254) Closing a terminal had half disappeared: the tab pill's hover close was gated on there being more than one tab (so the loop's own tab never offered it), a split pane had no close affordance anywhere, and ⌘W — bound to Close Tab — sat disabled exactly in the state ⌘D creates, one tab carrying two panes. And even where closing worked it only ended the attach: the zmx session behind a closed shell kept running, invisible, until reboot. - The tab pill's x shows on hover again, always; closing the workspace's last tab is the end of the loop itself — forwarded to AppFeature, which deletes the node the way the sidebar's delete does (daemon deleteNode, which also kills the session). A quick chat just puts the terminal away; the chat outlives its session. - A pane header grows its own hover x while its tab is split, sending the reducer's existing paneClosed. - ⌘W is Close Pane — the focused pane, the tab when it isn't split, the loop when it is the last — and Close Tab moves to ⇧⌘W. Titles follow what the keystroke will actually close. - Closing a pane or tab kills the zmx sessions behind the plain shells it carried (TerminalSurfaceClient.killSessions, remote-aware via projectPath); agent surfaces are exempt — the loop's session belongs to the node, not to any pane. - TerminalLayout.opening(forNode:saved:) restores the default agent tab when a saved layout lost the node's own surface, so closing the agent tab cannot make a running loop reopen as shells-only. Signed-off-by: scgopi <scgopireddy@gmail.com>
…ad of replacing them Review follow-ups on #262. - Closing the workspace's last tab no longer deletes the loop outright. It raises the same "Delete Loop…" confirmation every other delete in the app goes through (`deleteNodeRequested`, whose dialog `AppView` hosts). A fresh workspace has exactly one tab, so the x, ⌘W, ⇧⌘W — and a plain shell simply exiting, which sends `.paneClosed` with no gesture at all — each reached an irreversible delete of a possibly-running loop by reflex. - `.tabClosed` retires and kills nothing on the last-tab path. The answer may be no, and a tab whose surfaces were already retired and whose shells were already killed is not a tab anyone can be given back. It also removes the double teardown the old ordering caused, `closeOpenWorkspace` having walked the same untouched surface list a second time. - `TerminalLayout.opening` puts the agent tab back at the front of a saved layout rather than replacing the layout with the default. Replacing it dropped the saved shell tabs *without* killing their zmx sessions — the invisible-until-reboot leak `killSessions` exists to close. A selection left dangling lands on the restored tab. - One ⌘W in the menu bar. SwiftUI gives every `WindowGroup` a `File ▸ Close` at ⌘W and AppKit resolves key equivalents left to right, so File answered before the Terminal menu could; `CommandGroup(replacing: .saveItem)` frees the key and gives window close ⇧⌘W (Ghostty's pairing). Close Pane keeps a fixed title — one that renamed itself to "Close Tab" collided with the item below it — and Close Tab keeps its menu entry without a shortcut, since ⌘W already closes a tab that isn't split. - Tests for the branch that had none: last tab asks rather than deletes, cancelling keeps the loop, a chat's last tab only closes the workspace, a lone shell exiting asks too, and the layout heal keeps the tabs it repairs. In their own suite — `AppFeatureTests` sits on swiftlint's type_body_length ceiling. xcodebuild test: 1300 tests, 141 suites, 0 failures. swiftlint: 0 errors. swift-format --strict: clean (the standing GraphcodeCommandTests.swift errors predate this branch). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148d94ra6ghJ6fY1DFkHP5m
`.primarySurfaceExited` and `.stopLoopTapped` moved to `AppFeature+LoopSessions` on main, and layout loading centralised there in `mountWorkspace` — the agent-tab heal goes with it. `TerminalSurfaceClient` takes main's `retireAll` alongside this branch's `killSessions`. xcodebuild test: 1535 tests, 159 suites, 0 failures. swiftlint: 0 errors. swift-format --strict: clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148d94ra6ghJ6fY1DFkHP5m
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.
Closes #254
Problem
tabs.count > 1, so a loop's own (only) tab never offered an x — hovering the tab strip showed nothing..paneClosedexisted in the reducer but nothing in the UI or menus sent it.Changes
AppFeaturedeletes the node the way the sidebar's delete does (daemondeleteNode, which also kills the session). A quick chat just puts the terminal away; the chat outlives its session..paneClosed(collapse + focus handoff already handled there).TerminalSurfaceClient.killSessions(remote-aware via the workspace's project path) ends the zmx sessions behind plain shells on pane/tab close; agent surfaces are exempt — the loop's session belongs to the node, not to any pane, and ends when the node does.TerminalLayout.opening(forNode:saved:)restores the default agent tab when a saved layout lost the node's own surface, so closing the agent tab can't make a running loop reopen as shells-only.Verification
xcodebuild build: succeededxcodebuild test: 1294 tests, 0 failures (6 new: last-tab forwards to parent, shell-vs-agent kill rules for tab and pane close, layout heal)swiftlint: 0 errors ·swift-format --strict: clean