Skip to content

fix(coordinator): make the window title a function of the pane it is showing - #2054

Merged
datlechin merged 1 commit into
mainfrom
fix/window-title-phase
Aug 9, 2026
Merged

fix(coordinator): make the window title a function of the pane it is showing#2054
datlechin merged 1 commit into
mainfrom
fix/window-title-phase

Conversation

@datlechin

@datlechin datlechin commented Aug 9, 2026

Copy link
Copy Markdown
Member

The window title was the one part of the connection window that never became a function of its phase. Content and chrome already are, since #2053. The title was still decided by whoever wrote to it last.

Found by auditing every title path across four lenses (window lifecycle, compliance with the "Window tab titles" invariant in CLAUDE.md, behaviour under translation, test coverage), with each finding independently attacked before being accepted. 17 survived, 5 were refuted.

What was wrong

A connecting window was called "SQL Query". MainSplitViewController.init resolved the title from a payload that describes no tab, so resolveTitle fell through every tier to fallbackTitle, which is literally the string "SQL Query". The window was named after a document it did not have.

A window that lost its session kept the name of the table it had stopped showing. applyPhase() repaints content and chrome and never touched the title, and the only ongoing title sink lives inside MainContentView, which is unmounted the moment the pane stops being .content. The pane would read "Connection closed" while the window and its native tab label still said orders. Reachable through exhausted tunnel recovery, which is easy to hit after waking from sleep, and through an MCP disconnect.

Placeholder detection compared a title against English text. adoptSession decided a title was disposable by testing windowTitle == WindowTitleResolver.fallbackTitle || windowTitle.hasSuffix(" Query"). Both halves compare against the rendering of a localized string, so the whole branch is dead in every translated build. In English it was worse than dead: a tab a user named "Weekly Query" was silently renamed on every reconnect.

The titlebar repeated itself. That same branch set title and subtitle to the same value, and a second site set the title to the connection name while the subtitle was already the connection name. Hence "TablePro - TablePro".

The no-tabs rule existed twice and the two copies disagreed, in MainContentView.onAppear and in updateWindowTitleAndFileState().

Renaming a connection never reached an open window, because the window captured its connection record once at creation.

The fix

One resolver entry point decides title and subtitle together:

static func resolveWindow(pane:connection:tab:hasTabs:queryLanguageName:) -> ResolvedWindowTitle

Not showing content, or showing content with no tabs, resolves to the connection name with no subtitle. Showing a tab resolves to the tab's title with its database binding as subtitle, suppressed when it would equal the title. Deciding both together is what makes the repetition impossible by construction rather than by a guard someone has to remember.

applyPhase() gains a title sink beside content and chrome, so a phase change repaints the name. MainSplitViewController.init calls it after super.init, which keeps the invariant that a native tab label is correct at creation rather than at first activation, since AppKit draws labels for tabs that are never activated.

The string-matching block in adoptSession is deleted rather than repaired. So is the duplicated no-tabs rule in onAppear and the .newEmptyTab-only title override, which is why a query tab opened from a link or from AI used to keep "SQL Query" while the tab itself read "Query 3": the resolver now reads the real selected tab whatever the intent was.

payloadConnection is re-read when the connection record changes, so a rename reaches both the window name and the connecting screen.

Not a rewrite

WindowTitleResolver's existing tab logic is untouched and its 47 tests still pass. It was never what was broken. The defect was that consumers made title decisions the resolver did not own.

Testing

swiftlint --strict clean across 1291 files. Build succeeds. WindowTitleResolverWindowTests adds 7 tests covering what no test covered before, since every previous test exercised the pure resolver and nothing exercised a consumer-side decision: every non-content pane resolves to the connection name, restored tabs do not leak into the name of a window that is still connecting, an empty content window carries no subtitle, the subtitle never repeats the title, blank and missing connections still produce a usable name, and a tab a user named themselves survives a reconnect.

Deferred

One confirmed finding is left out on purpose. New query tab names are the only tab titles never run through localization, and the counter behind "Query 3" is recovered by parsing the text of existing tab titles (QueryTabManager.swift:107). That is the same class of defect as the placeholder matcher fixed here, but it belongs to tab naming rather than window naming and deserves its own change.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit b2d0d05 into main Aug 9, 2026
4 checks passed
@datlechin
datlechin deleted the fix/window-title-phase branch August 9, 2026 17:10
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.

1 participant