Skip to content

feat(connections): list saved connections under their groups in Switch Connection - #2620

Merged
datlechin merged 1 commit into
mainfrom
feat/grouped-connection-switcher
Sep 3, 2026
Merged

feat(connections): list saved connections under their groups in Switch Connection#2620
datlechin merged 1 commit into
mainfrom
feat/grouped-connection-switcher

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #1311.

What the issue asked for, and what is left of it

The issue is from May, and it predates the two refactors that answered most of it. #2035 added the
connections strip and #2097 made one window host every connection, so clicking a connection already
switches the window in place instead of piling up tabs, which was the reporter's actual complaint.

What none of that delivered is the first half of the request: the group hierarchy is invisible from
a main window. ConnectionGroup carries a parent, a colour and a sort order, and it syncs, but the
only surface that renders it is the welcome window. Switch Connection, the one place inside the
window that lists every saved connection, showed a flat list with the group reduced to a badge.

Why the panel and not the sidebar

The request as written is the DataGrip and DBeaver shape, one tree holding groups, connections,
databases, schemas and tables. Two things rule it out.

The HIG's sidebar page: "In general, show no more than two levels of hierarchy in a sidebar. When
a data hierarchy is deeper than two levels, consider using a split view interface that includes a
content list."
The sidebar already holds database, schema, object kind and table. Groups and
connections on top of that is six levels.

And no macOS-native client does it. TablePlus keeps saved connections on its welcome screen and its
second sidebar lists only what is open, gated on there being more than one, which is the same shape
as our connections strip. Postico, Sequel Ace and Querious are the same. The two that merge the
trees are cross-platform IDEs.

The strip cannot carry it either, and not for want of room: WorkspaceRailStore.entries is derived
from the windows that host a connection, and WorkspaceID is keyed on a connection plus the
container it is browsing. A connection that is not open has neither.

So the hierarchy goes where the library already lives.

What changed

  • Switch Connection lists saved connections under their groups, in the order the connection list
    shows them, a nested group naming its whole path (ACME / EUROPE) and each header carrying the
    group's own colour. Connections in no group come last, which is where the tree puts them.
  • A group with nothing under it contributes no section. The rendered list was already right, since
    an empty section draws no header, but the model claimed a hierarchy that was not on screen and
    that is what decides the last section's name: with no group visible it stays SAVED
    CONNECTIONS
    , because "ungrouped" needs a group beside it to mean anything. A parent holding
    only subgroups names itself through its children's path instead of drawing an empty header.
  • Open connections keep their own section at the top. Burying one in its group would put the two
    connections a user switches between furthest apart, and the section already excludes them from
    the saved half, so nothing is listed twice.
  • Typing collapses the groups back into one list. A search is a lookup rather than a browse, and one
    match in each of eight groups is eight one-row sections. It also keeps the arrow keys away from
    header rows, which is TablePlus issue fix(plugin-duckdb): render timestamp and nested column types the value API cannot decode #2134, open since 2020.
  • Cmd-click opens a saved connection in a window of its own, the modifier Finder and Safari use
    for the same intent. A connection already open is switched to either way: moving one between
    windows belongs to the connections strip, which owns that arrangement.
  • "Already open" is a window hosting the connection, not an entry in activeSessions, and that
    now decides both halves. A workspace outlives its session, so a connect that failed, one the user
    cancelled and an explicit disconnect all leave a connection open with no session; the old rule
    listed those under a group, where Cmd-click promised a window they would never get, and in a
    shared window the ordinary route then switched away from the connection on screen to reconnect a
    hidden one. The top section is derived from WindowManager.hostedWorkspaces() and the saved rows
    are what is left, with connectionWindowsChanged observed so the split follows a window opening
    or closing. That is CLAUDE.md's "A workspace's content is a function of its own
    ConnectionWindowPhase, never of activeSessions membership".
  • The host check lives in the router, next to the window it builds. Deciding it in the click
    handler left a main-actor job between the question and the answer, and anything that opened the
    same connection in between, the MCP tool among them, would have left two workspaces restoring the
    same tabs. Nothing is awaited between the two now.
  • openInNewWindow gained joinsTabGroup. Left alone it joins the existing tab group on purpose,
    which is what once made Open in New Window produce a native tab of the window it was asked to
    leave, so the switcher passes false and gets a window that stands apart. Tabbing is refused for
    the moment the window is placed and allowed again straight after, so standing apart now does not
    cost it the right to be merged by hand later.
  • A section header is identified by what it draws, not by its section id. Only item rows are
    refreshed in place, so a group renamed or recoloured on another device kept its old header on
    screen until something structural forced a full reload.
  • The panel follows connectionUpdated instead of reading storage once in onAppear, so a group
    renamed or a connection added while it is open is reflected. That event only exists on the group
    path since fix(connections): make the group system report its refusals and survive a broken graph #2619. It subscribes to the subject itself rather than a receive(on:) wrapper, which
    would build a new publisher on every body pass; every sender is already on the main actor.
  • FieldDrivenListSection gained an optional accent colour, drawn as a dot beside the header title.
    It defaults to nil, so the database switcher and the history pane are untouched.

Non-goal: collapsing a group

This is a transient panel with a search field, and neither Spotlight nor Xcode's Open Quickly lets
you fold a section away. Collapsing would need a click path to a header row, which
clickedItemId() cannot reach because a header carries no item id, plus somewhere to persist the
state, logic to hide the descendants of a collapsed group, and an auto-expand that walks the whole
ancestor chain of the active connection. If it turns out to be wanted, the honest answer is an
NSOutlineView on the existing SidebarOutlineScaffold rather than bolting it onto a table.

What the new-window path deliberately does not do

It never prompts for a pre-connect script. A window whose connection carries one does not
auto-connect at all: MainSplitViewController+Connection.swift:37 sends it to its not-connected
state, where Connect asks. Prompting in the router as well would ask the same question twice and
the first answer would change nothing, which is why the window-opening half of openConnection has
never called it either.

Verification

  • verify.sh generate, verify.sh build: PASS
  • verify.sh test ConnectionSwitcherFilterTests ConnectionSwitcherSectionsTests StringCatalogIntegrityTests: PASS
  • verify.sh uitest SwitcherEscapeUITests: PASS
  • verify.sh docs: PASS, both the house-style and the source-claim check
  • swiftlint --strict on every changed file: clean

Section building moved out of the view into ConnectionSwitcherSections, so the part that decides
what is listed and in what order is testable without a window. Six new cases cover group ordering,
the path title of a nested group, the accent colour, the flattening under a filter, a group whose
connections are all open drawing no rows, and the row order the arrow keys walk matching the
sections.

No new UI automation. Seeding a group tree needs a launch hook the app does not have: the only two
it exposes are the sandbox root and "open the sample database", and a UI test target cannot import
the app to write the connection store itself. Adding one would be production code with no user. The
panel itself stays covered by SwitcherEscapeUITests, which drives it through real key presses.

https://claude.ai/code/session_01L81TaoPWxkLd15CGw2riPq

@mintlify

mintlify Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Sep 3, 2026, 7:48 AM

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

@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 dc08f83 into main Sep 3, 2026
8 of 9 checks passed
@datlechin
datlechin deleted the feat/grouped-connection-switcher branch September 3, 2026 07:49
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.

Feature Request: Show connection groups and their connections in the sidebar with in-tab switching

1 participant