Skip to content

Show close UX for terminal tabs and split panes; ⌘W ends the session (#254) - #262

Merged
scgopi merged 3 commits into
mainfrom
issue-254-close-ux
Sep 3, 2026
Merged

Show close UX for terminal tabs and split panes; ⌘W ends the session (#254)#262
scgopi merged 3 commits into
mainfrom
issue-254-close-ux

Conversation

@scgopi

@scgopi scgopi commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Closes #254

Problem

  • The tab pill's hover close was gated on tabs.count > 1, so a loop's own (only) tab never offered an x — hovering the tab strip showed nothing.
  • A ⌘D split pane had no close affordance at all: .paneClosed existed in the reducer but nothing in the UI or menus sent it.
  • ⌘W was bound to Close Tab and disabled with one tab — exactly the state ⌘D leaves you in — so there was no way to close.
  • Even where closing worked, it only retired the surface (ended the attach); the zmx session behind a closed shell kept running detached until reboot.

Changes

  • Tab pill x always shows on hover — including a lone tab. Closing the workspace's last tab now ends the loop itself: AppFeature 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.
  • Pane headers get a hover x while their tab is split, sending the reducer's existing .paneClosed (collapse + focus handoff already handled there).
  • ⌘W is Close Pane — the focused pane, the tab when it isn't split, the loop when it's the last — and Close Tab moves to ⇧⌘W. The menu title follows what the keystroke will actually close.
  • Closed shells die with their panes: 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.
  • Self-healing reopen: 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: succeeded
  • xcodebuild 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

scgopi and others added 3 commits September 2, 2026 20:22
…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
@scgopi
scgopi merged commit ea09745 into main Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Close button not showing on terminal tabs

1 participant