Skip to content

feat(macos): per-profile provider-connection picker#30271

Merged
noanflaherty merged 2 commits into
mainfrom
credence/macos-profile-connection-picker
May 11, 2026
Merged

feat(macos): per-profile provider-connection picker#30271
noanflaherty merged 2 commits into
mainfrom
credence/macos-profile-connection-picker

Conversation

@credence-the-bot
Copy link
Copy Markdown
Contributor

@credence-the-bot credence-the-bot Bot commented May 11, 2026

What

Closes audit finding #5: profile editor cannot bind to a specific provider connection.

The daemon schema already accepts provider_connection on ProfileEntry (see assistant/src/config/schemas/llm.ts line ~340). This PR plumbs the field through the macOS model + editor so a user with multiple openai (or anthropic / gemini / ...) connections can route a profile to a specific row instead of relying on the dispatcher's "first active wins" fallback.

UX

  • New Connection sub-dropdown renders below Provider when the selected provider has at least one active connection.
  • First option is "Any active <provider> connection" (value=""), preserving the daemon's legacy first-active-wins behavior.
  • Subsequent options are the active connections, labelled by label (fallback: name).
  • Changing the Provider clears the connection binding so a stale name never survives a provider switch.
  • A "Not found" warning badge surfaces when the saved binding no longer matches any active connection (most often: a connection was disabled or deleted outside the editor).

Wire shape

  • JSON key: provider_connection (snake_case — matches the Zod schema)
  • Swift property: providerConnection (camelCase)
  • preservedJSON exclusion list updated so the snake_case key doesn't double-write.

Fetching

Connections are fetched once by InferenceProfilesSheet.task and re-fetched when the editor closes (to pick up a connection added in another surface during the same session). Tolerant on transport failure (stale list preserved) — same posture as SettingsStore.providerKeys.

Tests

  • 4 new InferenceProfileTests: wire-shape round-trip, empty-string decode-as-nil, merging-from-fragment, merging-preserves.
  • 5 new InferenceProfileEditorTests: per-provider + per-status filter, empty-state behaviors (no provider, no connections), connectionDisplayName helper (label preferred, fallback to name), full body construction with populated connections list.
  • InferenceProfilesSheetTests updated to inject the mock client.

No daemon changes

The wire field already exists in the Zod schema. The daemon's resolver already routes via provider_connection when set. This is a UI-only change on the macOS side.

🤖 Generated with Claude Code


Open in Devin Review

Closes audit finding #5 (profile editor cannot bind to a specific
provider connection). The daemon schema already accepts
`provider_connection` on `ProfileEntry` (see
`assistant/src/config/schemas/llm.ts`); this PR plumbs the field
through the macOS model + editor.

UX:
- New "Connection" sub-dropdown renders below Provider when the
  selected provider has at least one active connection. First option
  is "Any active <provider> connection" (value=""), preserving the
  daemon's legacy first-active-wins behavior. Subsequent options are
  the active connections, labelled by `label` (fallback: `name`).
- Changing the Provider clears the connection binding so a stale
  name never survives a provider switch.
- A "Not found" warning badge surfaces when the saved binding no
  longer matches any active connection (most often: a connection was
  disabled or deleted outside the editor).

Wire shape: snake_case `provider_connection` for the JSON key (matches
the Zod schema); camelCase `providerConnection` for the Swift
property. `preservedJSON` exclusion list updated so the snake_case
key doesn't double-write.

Connections are fetched once by `InferenceProfilesSheet.task` and
re-fetched when the editor closes (in case a connection was added in
another surface during the session). Tolerant on transport failure
(stale list preserved) — same posture as `SettingsStore.providerKeys`.

Tests:
- 4 new `InferenceProfileTests` cases cover wire-shape round-trip,
  empty-string decode, merging-from-fragment, merging-preserves.
- 5 new `InferenceProfileEditorTests` cases cover the per-provider
  filter, empty-state behaviors, the display-name helper, and the
  full body construction with a populated `connections` list.
- `InferenceProfilesSheetTests` updated to inject the mock client.

No daemon changes required.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0375fae54

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ViewBuilder
private var connectionField: some View {
let available = availableConnectionsForProvider
if !available.isEmpty, let provider = profile.provider {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep connection picker visible for stale bindings

The Connection field is hidden whenever there are no active matches (if !available.isEmpty), which means a profile that already has providerConnection set to a now-disabled/deleted row cannot be cleared from this editor. In single-provider workspaces the user may have no alternate provider to trigger the provider-change reset path, so the profile can remain stuck with an invalid binding and continue failing connection resolution. Consider rendering this field (with the existing "Not found" warning) whenever a binding exists, even if available is empty.

Useful? React with 👍 / 👎.

if let label { result["label"] = label }
if let profileDescription { result["description"] = profileDescription }
if let provider { result["provider"] = provider }
if let providerConnection { result["provider_connection"] = providerConnection }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve clear semantics for provider_connection on save

When the user chooses “Any active … connection”, providerConnection becomes nil, and toJSON() omits provider_connection. However, the profile-replace route only clears keys listed in INFERENCE_PROFILE_UI_KEYS (in assistant/src/runtime/routes/conversation-query-routes.ts), and that set does not include provider_connection, so the old server-side binding is retained. The UI then appears cleared while runtime routing still uses the stale connection. Add an explicit clear mechanism for this field (or include it in the server-side UI key clear set).

Useful? React with 👍 / 👎.

…_connection in route

Codex flagged two P1s on the original PR:

1. **Picker hidden for stale bindings.** When a profile's
   `providerConnection` referenced a now-disabled or deleted connection
   AND the workspace had no active connections for that provider, the
   editor hid the entire Connection field. The user had no way to clear
   the broken binding from the UI, and the profile remained pinned to an
   invalid name. Refactor: visibility now gates on (active matches OR
   non-empty saved binding); when stale, the picker shows a
   '<name> (not found)' option so the trigger renders the bound name,
   and the Not Found badge surfaces the state. Selecting 'Any active …'
   clears the binding back to the daemon's first-active fallback.

2. **`provider_connection` not in `INFERENCE_PROFILE_UI_KEYS`.** The
   profile-replace route only clears keys listed in that set, so
   omitting `provider_connection` from the body (which is what the UI
   does when the user picks 'Any active') silently retained the old
   server-side binding. Added `provider_connection` so the route now
   correctly wipes the stored value when the UI clears it.

Tests:
- 5 new InferenceProfileEditorTests covering staleProviderConnection
  truth table (binding missing / disabled / matches / nil / empty) +
  body-builds-with-stale.
- 2 new conversation-query-routes tests covering write + clear paths
  for `provider_connection` on PUT /v1/config/llm/profiles/:name.

13/13 daemon tests pass; assistant tsc + eslint clean.

Addresses Codex P1 #1 + P1 #2 from `@codex review` on `f0375fae54`.
@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@codex review
@devin review

Round 2 — addressed both P1 findings.

Commit: b35d2f2e2c

P1: Keep connection picker visible for stale bindings. Refactored visibility to gate on (active matches OR non-empty saved binding). When the binding is stale, the dropdown surfaces the bound name as a <name> (not found) option so the trigger renders it, and the "Not found" badge flags the state. Picking "Any active …" clears the binding back to the daemon's first-active fallback.

P1: Preserve clear semantics for provider_connection on save. Added provider_connection to INFERENCE_PROFILE_UI_KEYS so the profile-replace route now correctly wipes the stored value when the UI omits it from the body.

Tests added:

  • 5 InferenceProfileEditorTests covering staleProviderConnection truth table + body-builds with stale binding.
  • 2 conversation-query-routes tests covering write + clear paths.
  • 13/13 daemon tests pass; assistant tsc + eslint clean.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@noanflaherty noanflaherty merged commit ff1ed98 into main May 11, 2026
13 checks passed
@noanflaherty noanflaherty deleted the credence/macos-profile-connection-picker branch May 11, 2026 15:06
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