Skip to content

refactor(toolbar): make the connection window's chrome a function of what it is showing - #3041

Merged
datlechin merged 9 commits into
mainfrom
ui-revamp-connection-window
Sep 21, 2026
Merged

datlechin merged 9 commits into
mainfrom
ui-revamp-connection-window

Conversation

@datlechin

@datlechin datlechin commented Sep 21, 2026

Copy link
Copy Markdown
Member

The connection window's titlebar carried the same seventeen controls whatever you were doing, and #2384 added two more to it. This makes the chrome a function of what the window is showing.

Root cause

The toolbar had no model of the window. ValidationContext carried one tab-shaped fact, isTableTab, and no content mode at all, so one list of fifteen identifiers expanding to seventeen hit targets was vended for all eight tab kinds, all four panes and both content modes, and the only lever anyone had was dimming. Every feature that arrived had to buy a permanent slot. The guard test written to stop exactly that counted identifiers rather than hit targets, so #2384's two-segment control passed it.

The second half is that the toolbar was trying to be three things at once: the window's identity, the tab's verbs, and a mode switch, with no zone meaning anything.

Shape

One toolbar for the life of the window, one fixed identifier list, and context expressed by NSToolbarItem.isHidden and isEnabled and nothing else. Two pure resolvers decide it:

  • ToolbarContextResolver maps a ToolbarContext to the hidden set and to each item's enablement, exhaustive over TabType with no default: arm.
  • ConnectionActionsMenuResolver maps the same context to one Actions pull-down carrying the long tail, built fresh on every open with target = nil so the responder chain and validateMenuItem gate it exactly as they gate the menu bar.

The rule that makes it safe: isHidden is written only from ToolbarContext.VisibilityKey (tab kind, results mode, content mode, driver capability); isEnabled carries everything transient. So the titlebar can change shape on a tab switch, a mode switch or a connection switch and at no other moment. A staged edit, a running query or a dropped connection dims a control and never moves one.

Context Controls
Browse, table or query tab 8
Create Table, ER diagram, Server Dashboard, Query Insights, DDL 7
SQLite or DuckDB, or any engine with one container 7
Agent mode 6
macOS 13 and 14 the same 8 everywhere, dimming what cannot act

An item you added yourself through Customize Toolbar is never hidden. Only identifiers in defaultItemIdentifiers are hideable, so a button you placed stays where you put it and merely dims.

Measured, not assumed

Probes compiled and run on macOS 27. Every one of these decided a design choice:

  • isHidden reclaims the item's full width, writes nothing to disk, is not autosaved, and costs 0.003ms per toggle. insertItem/removeItem write both TB Item Identifiers and TB Default Item Identifiers immediately, and a second window sharing the identifier sees it in the same run loop turn; every connection window shares one identifier, so that approach was out. setConfigurationFromDictionary is deprecated 10.0 to 15.0 and unavailable from Swift.
  • A hidden item leaves the overflow menu and the accessibility tree (15 elements to 13). So the overflow is not a safety net: every hideable command has a menu-bar home, which is what step 5 finishes.
  • The isHidden setter runs no validation, and a hidden item's isEnabled freezes until something revalidates. Every write is followed by validateVisibleItems().
  • One visit to Customize Toolbar permanently poisons NSToolbar.visibleItems and NSToolbarItem.isVisible; both over-report and nothing repairs them. ToolbarSwitcherPresenter.anchor no longer reads either, and a source-scanning guard test fails the build if anything does.
  • Anchoring a popover to a group subitem whose group is off screen raises an uncatchable NSInvalidArgumentException. The centred pair is therefore two top-level items rather than a group: 0 raises across 16 presentations where the group raised 4 of 18, anchors within 2pt, and 249pt against the group's 257pt.
  • AppKit splices a new default identifier at its position in the default list, not at the end, and prunes an identifier the delegate stops vending. So the toolbar identifier does not bump, and a customized arrangement survives.

That last one is visible in the screenshots below: this branch was photographed on a machine whose owner had dragged Back, Forward and Assistant into the toolbar by hand. Back and Forward survived; contentModeItem, refreshSaveGroup, editorGroup, connectionGroup and the throughput readout were pruned; and connection, database, refresh, saveChanges and actions were spliced into their default positions. A bump would have thrown that arrangement away.

Before and after

Same machine, same window size, same saved arrangement, only the code differs. Back and Forward are that user's own additions and survive. The Assistant button in the after shot was also theirs, and a later commit retired that item, so it is pruned on the next launch: the pane toggle opens the column and the picker in the pane's header chooses what it draws.

Before, 18 hit targets. Chinook.sqlite is the container capsule, dim and inert on SQLite.

Toolbar before

After, 10 at the time of the shot, of which 3 were the user's own. A fresh install shows 7 here, and 9 remain for this user once the Assistant item is pruned.

Toolbar after

Window before

Window after

Agent mode, with the Actions pull-down open on the session commands:

Agent mode

What moved, and where it went

Every command that left the default set has a menu home and a rebindable action. Nothing is only in the toolbar.

Command Now
Tables, Favorites A segmented control at the top of the sidebar, over the list it drives, plus View > Show Tables / Show Favorites, both rebindable for the first time
Browse, Agent View > Mode, ⌥⇧⌘A, the welcome window's Open in Agent Mode, and the Actions menu
Back, Forward Actions on a table tab, View > Back / Forward, ⌃⌘[ and ⌃⌘], and Customize Toolbar
Add Row, Restore Previous Values Actions, Edit menu, the grid's context menu
New Tab, Open Quickly Actions, File menu, the tab strip's + button
Preview SQL, Show Results, Export, Import, Server Dashboard, Query History Actions, and their own menus
Import format list Actions > Import Data From, and File > Import gains the same submenu, so the menu bar stops always taking the first format
Assistant The one trailing-pane toggle plus the pane header's picker. It has no toolbar item at all, in either list
Throughput readout The connection switcher's footer, where it already shipped
Agent sessions A new File > Session submenu, the Actions menu, and the rail

Defects fixed

Thirty, found while inventorying the old shape. The ones a user would notice:

Defect Fix
Save Changes and ⌘S permanently dim on a Users & Roles tab with staged principal edits, although saveChanges() already carried the branch that applies them hasPrincipalChanges joins updateToolbarPendingState() and PendingChangeTrigger, through a new PendingChangeKind
Entering or leaving Agent mode destroyed the browse tree: grid scroll, rectangular selection, the editor's find panel and undo stack, and an unsaved Create Table definition The agent panes become long-lived hosting controllers in WorkspacePanes; the mode toggle is a reparent, not a rebuild
No route anywhere in the app to delete an agent session, and a session could be opened only by double-clicking, which publishes no accessibility action and costs a measured 371ms on every single click A source-list + and , Return, a per-row accessibility action, a context menu, and File > Session
A session closed from the rail stayed on screen with a live composer Close and Delete route through the controller and repaint every window hosting the connection
The agent Result column said "This query returned no rows." for an approved UPDATE and for a payload it could not read The decoder returns a kind, and the column draws four answers
Two of the Result column's four segments could never show anything The picker is SQL and Results
Show Results enabled on the tab kinds that have no results pane, then writing a collapse flag with no tab-kind guard Answered per tab kind, and the menu arm adopts the same rule
Query History enabled and inert over a window that never connected, and ⌘Y in Agent mode flipping a persisted flag for a drawer that is not mounted isConnected && contentMode == .browse on both surfaces
Any toolbar identifier nobody had thought about was enabled, including over a window with no session The resolver is exhaustive; the default: arm answers false
The first grid click overwrote a trailing pane the user had left on the Assistant Xcode's explicit-preference rule: a surface the app suggests is not stored
Agent mode raised Safe Mode silently, and picking a lower level persisted it with no visible effect The floor is read by the menu and the toolbar, the reason is shown, and a level below it is refused
File > Import always took the driver's first format Both surfaces carry the format list
Two per-connection preferences escaped the UI-test sandbox and were never purged when a connection was deleted Both move to AppStorageEnvironment and join ConnectionLocalState.purge
The titlebar kept a query tab's file icon after the connection dropped, or from another connection in the window The proxy icon goes through WindowTitleResolver and the same guarded sink as the title
Two middle-dot separators and a coloured identity dot in the history drawer House rule: tinted glyph, secondary info on its own label

The full thirty are in the commit messages; the ones above are the released ones. Agent mode is unreleased, so its repairs carry no CHANGELOG Fixed line.

Verification

  • Build: app, TableProTests and TableProUITests all compile, Debug, into a private DerivedData.
  • Unit: 1,213 cases across 122 suites, 0 failures, on the rebased tree. New suites: ToolbarContextResolverTests, ConnectionActionsMenuResolverTests, ConnectionActionsMenuDelegateTests, ToolbarHiddenSetTests, ToolbarSourceAccessTests, MenuContentModeParityTests, SessionMenuDelegateTests, PendingChangeKindTests, TrailingPaneSurfaceResolverTests, TrailingPaneHeaderModelTests, TrailingPaneCommandTitleTests, TrailingPaneRevealTests, AgentModeWindowTests, AgentArtifactCacheTests, AgentSessionConfirmationTests, BrowseCollapseStateOwnershipTests, SidebarScopeControlTests, HistoryRowTintTests.
  • Lint: swiftlint --strict, 0 violations.
  • Docs: verify.sh docs PASS; 13 house-style checks, 8 source-claim checks, 135 pages with every link resolving.
  • Screenshots: captured from Debug builds of this branch and of main, both driven against throwaway sandboxes.

Not verified

  • TableProUITests were compiled but never run. The runner cannot start on this machine: it dies before any test begins, on untouched suites too. The new cases in ConnectionWindowChromeUITests, AgentSessionRailUITests, TrailingPaneSurfaceUITests, AgentModeRoundTripUITests and AgentModeMenuUITests are for CI. Two of the toolbar cases skip below a 1512pt screen, so on a 1024pt runner only the sidebar-scope case executes.
  • macOS 13 and 14 were not run. isHidden is macOS 15, so the fallback (the union of eight, dimming what cannot act) is derived from source and from apply(_:)'s availability guard, not observed.
  • A real Customize Toolbar drag was not driven. A drag inside a modal sheet could not be synthesized; insertItem(withItemIdentifier:at:) was, and it posts willAddItemNotification once with the item. If a real drop does not post, a dragged-in core item keeps the state it was vended with until the next tab switch, and the fresh-vend rule makes that state visible rather than invisible.
  • Manual pass wanted before merge: drag Refresh out through Customize Toolbar and back, on a Create Table tab and on a table tab; and Save Changes on a Users & Roles tab against a real server, since that branch of saveChanges() has never run from the toolbar or from ⌘S before.

Deliberately out of scope

  • Query History stays a drawer under the tab content. It is a seven-control filter row over a nested list-and-detail split, and 270pt cannot hold it. It gains the content-mode gate and a route in the Actions menu.
  • No per-tab command bar above the data grid. The titlebar is height the window pays for anyway; a command bar is height taken from rows.
  • The database chooser, Safe Mode, Export, Import and Server Dashboard do not move into the sidebar footer. The sidebar collapses, and Safe Mode is the control that answers "am I one keystroke from writing to production".
  • Focus Inspector and Focus Assistant stay enabled over a trailing pane showing the not-connected view, where they focus nothing. Pre-existing, and not caused by this change.
  • QuickSwitcherPanelView still uses the banned .onTapGesture(count: 2). Outside this window's chrome.
  • The middle-dot separator survives in about twenty call sites outside this diff.

@mintlify

mintlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 21, 2026, 7:15 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit eb1a4b4 into main Sep 21, 2026
7 checks passed
@datlechin
datlechin deleted the ui-revamp-connection-window branch September 21, 2026 19:16
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