Skip to content

Add tab renaming#269

Merged
sbertix merged 7 commits intosupabitapp:mainfrom
bitomule:bitomule/tab-rename
May 5, 2026
Merged

Add tab renaming#269
sbertix merged 7 commits intosupabitapp:mainfrom
bitomule:bitomule/tab-rename

Conversation

@bitomule
Copy link
Copy Markdown
Contributor

@bitomule bitomule commented Apr 23, 2026

What

Enable renaming of tabs.

You can double tap the label or use the context menu option. I didn't understand the script tabs blocking so I tried not to interfere with any of that. Name is persisted so future sessions keep the custom name.

Disclaimer

I read you prefer issues than vibe coded PRs. I used claude code on this one but manually supervised plan, code and tested manually. I think issue is small enough and a PR is helpfull in this case. Otherwise please let me know.

How it works

Model layer

  • TerminalTabItem gains customTitle: String? and a computed displayTitle: String (customTitle ?? title). isTitleLocked is left exclusively for blocking-script tabs — no semantic change.
  • TerminalTabManager.updateTitle() gains a customTitle == nil guard so Ghostty surface-title callbacks don't overwrite a user rename.
  • TerminalTabManager.setCustomTitle(_:title:) sets/clears the override without touching isTitleLocked.

Persistence

  • TerminalLayoutSnapshot.TabSnapshot gains customTitle: String?. The field is Optional so existing layouts.json files without the key decode to nil — zero migration cost.
  • captureLayoutSnapshot() serialises customTitle (nil for blocking-script tabs, same as the existing icon/tintColor normalisation).
  • restoreFromSnapshot() calls setCustomTitle after recreating the tab if the snapshot carries a custom name.

UI

  • TerminalTabLabelView renders tab.displayTitle instead of tab.title.
  • TerminalTabView adds a @FocusState-managed TextField overlay that appears on double-tap (TapGesture(count: 2)). Enter commits, Escape cancels, focus loss commits. The label and close button are hidden (opacity: 0 / allowsHitTesting: false) while the field is active so there's no visual bleed-through.
  • The rename action is threaded from WorktreeTerminalTabsViewTerminalTabBarViewTerminalTabsViewTerminalTabsRowView following the same closure-chain pattern as closeTab / closeOthers.
  • TerminalTabContextMenu shows "Rename Tab" for non-locked tabs, wired to editingTabId state in TerminalTabsRowView.

Tests

  • 4 new TerminalTabManagerTests: custom title overrides display title, does not set isTitleLocked, blocks Ghostty updates, clears correctly.
  • 2 new TerminalLayoutSnapshotTests: customTitle round-trips through JSON encoding; missing key in legacy JSON decodes as nil.

Smoke test checklist

  • Double-click a tab → TextField appears pre-filled with current title
  • Type new name + Enter → tab shows custom name
  • Run vim → custom name stays, Ghostty title ignored
  • Escape during edit → reverts, no change applied
  • Submit empty string → no change
  • Double-click a blocking-script tab (Archive/Delete) → no edit field
  • Right-click → "Rename Tab" appears for user tabs, absent for script tabs
  • Quit + reopen → custom names survive across sessions

Tabs can now be renamed by double-clicking (or via right-click "Rename
Tab"). The custom name is preserved across sessions in layouts.json and
survives Ghostty shell title updates. Blocking-script tabs (Archive,
Delete, user scripts) are excluded from renaming via the existing
isTitleLocked flag.

- TerminalTabItem: add customTitle (user override) and displayTitle
  (computed: customTitle ?? title)
- TerminalTabManager: add setCustomTitle(); guard updateTitle() against
  overwriting a user-set name (customTitle == nil check)
- TerminalLayoutSnapshot.TabSnapshot: add customTitle field; Optional
  ensures zero-migration backward compat with existing layouts.json
- WorktreeTerminalState: persist and restore customTitle in
  captureLayoutSnapshot / restoreFromSnapshot
- TerminalTabView: double-tap gesture + inline TextField overlay;
  Enter commits, Escape/focus-loss guard prevents spurious saves
- TerminalTabContextMenu: "Rename Tab" entry for non-locked tabs
- Tests: 4 new TerminalTabManagerTests + 2 new
  TerminalLayoutSnapshotTests covering the rename and persistence paths
@bitomule bitomule marked this pull request as draft April 23, 2026 11:04
@bitomule bitomule marked this pull request as ready for review April 23, 2026 11:10
@bitomule bitomule changed the title Add double-click tab rename with session persistence Add tab renaming Apr 23, 2026
Copy link
Copy Markdown
Collaborator

@sbertix sbertix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works overall 💪, but a few things to sort before merge.

The guard in updateTitle means the second field isn't really earning its keep, the rename validation lives in the view instead of the manager, the blocking-script scrub at capture is untested, and the "Change Terminal Title" palette entry currently shows up but is a no-op, we should wire it in this PR.

Command palette wiring

The "Change Terminal Title" entry shows up in the command palette today, cause we pipe Ghostty's native commands through via .ghosttyCommand (see CommandPaletteFeature.swift around line 462/571/672), but it's a no-op: it routes to Ghostty's own title setter, which updateTitle now ignores once a customTitle is set, and it has no path to setCustomTitle at all. So users see the action and nothing happens, which is worse than not having it.

Could we intercept that specific Ghostty command in this PR and route it to the same rename flow (either trigger the inline TextField overlay for the active tab, or open a mini input popover)? Otherwise we ship a broken entry point on day one.

Comment thread supacode/Features/Terminal/Models/TerminalTabManager.swift Outdated
Comment thread supacode/Features/Terminal/Models/TerminalTabManager.swift Outdated
Comment thread supacode/Features/Terminal/Models/WorktreeTerminalState.swift
Comment thread supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift
Comment thread supacodeTests/TerminalTabManagerTests.swift
- Remove customTitle guard from updateTitle so Ghostty keeps writing
  to title even when a custom name is set. displayTitle still shows
  the custom name via customTitle ?? title.
- Move empty/whitespace normalisation into setCustomTitle (String, not
  String?) so the manager owns the invariant — no caller needs to decide
  what empty means.
- commitRename now always calls onRename(trimmed); empty string bubbles
  up to the manager which converts it to nil, clearing the custom title
  and snapping back to the live shell title (natural undo-rename).
- Update tests: ghosttyUpdate test now also asserts title is written;
  nil calls replaced with "" to match the new non-optional signature;
  new clearingCustomTitleRestoresLiveShellTitle test covers the full flow.
@bitomule bitomule requested a review from sbertix April 25, 2026 20:45
sbertix added 2 commits May 5, 2026 17:42
- Route prompt_surface_title / prompt_tab_title to the rename flow with
  synchronous tabID capture, so a tab switch between dispatch and effect
  cannot redirect the rename.
- Collapse pendingRenameTabID + view @State into a single editingTabID on
  TerminalTabManager; tabs.didSet drops it across every close path.
- Single-source the rename commit on the isEditing transition; .onDisappear
  commits on view-tree teardown (e.g. worktree swap) and skips when the
  user pressed Escape or left the buffer unchanged.
- setCustomTitle now guards against locked tabs and trims
  .whitespacesAndNewlines; snapshot capture keys on blockingScripts (not
  the isTitleLocked || tintColor heuristic).
- Add tests for the new contracts: synchronous tabID, no-tabs no-op,
  same-value pinning, snapshot round-trip with customTitle stripping.
@sbertix sbertix enabled auto-merge (squash) May 5, 2026 15:42
@sbertix sbertix disabled auto-merge May 5, 2026 15:43
@sbertix sbertix enabled auto-merge (squash) May 5, 2026 15:43
Copy link
Copy Markdown
Collaborator

@sbertix sbertix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you 🙇‍♂️

@sbertix sbertix merged commit 6615f49 into supabitapp:main May 5, 2026
2 checks 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.

3 participants