Skip to content

feat(ai): add ChatGPT subscription provider via Codex CLI OAuth flow#53

Merged
thinhntq merged 6 commits into
gridex:mainfrom
cyenxchen:feat/chatgpt-oauth-provider
Apr 30, 2026
Merged

feat(ai): add ChatGPT subscription provider via Codex CLI OAuth flow#53
thinhntq merged 6 commits into
gridex:mainfrom
cyenxchen:feat/chatgpt-oauth-provider

Conversation

@cyenxchen
Copy link
Copy Markdown
Contributor

@cyenxchen cyenxchen commented Apr 30, 2026

Overview

Adds a new AI provider type — OpenAI (Sign in) — letting users authenticate
with their paid ChatGPT subscription instead of supplying an API key. Internally
this reuses the Codex CLI public OAuth client
to obtain Bearer tokens, then talks to chatgpt.com/backend-api/codex/responses
with SSE streaming.

For users who already pay for ChatGPT Plus / Pro / Team this avoids the
per-token API spend and the friction of generating + rotating an API key.

Screenshots

1. New "OpenAI (Sign in)" entry in the provider type picker

PixPin_2026-04-30_17-09-36

2. Add Provider sheet — Authentication block before sign-in

PixPin_2026-04-30_17-09-53

3. Edit Provider sheet — signed in, models loaded from /backend-api/codex/models

PixPin_2026-04-30_17-10-53

How it works

  1. User picks OpenAI (Sign in) in the Add Provider sheet.
  2. Click Sign in with OpenAI → app opens the system browser at
    auth.openai.com/authorize with PKCE (RFC 7636) parameters.
  3. After consent, OpenAI redirects to http://localhost:1455/auth/callback
    a single-shot loopback HTTP listener inside the app accepts the redirect.
  4. App exchanges the auth code for access_token / refresh_token /
    id_token, decodes the JWT for account metadata (email, plan), and stores
    the bundle in the macOS Keychain under ai.chatgpt.tokens.<provider-uuid>.
  5. On every request, ChatGPTOAuthService.tokenBundle(...) checks exp and
    refreshes via auth.openai.com/oauth/token if within the skew window.
    Concurrent refreshes coalesce into a single Task to avoid token thrash.
  6. ChatGPTProvider streams /responses SSE deltas. A 401/403 from either
    /responses or /models purges the keychain bundle and the UI reverts
    to the signed-out state.

Architecture — 4 atomic commits

Split for bisect-friendliness; each commit compiles standalone.

  1. feat(ai-core) — PKCE verifier/challenge, JWT payload decoder,
    ChatGPTTokenBundle value type. No outward dependencies.
  2. feat(keychain)KeychainService extension
    (save/load/deleteChatGPTTokens) keyed by provider UUID, plus unit tests
    for the primitives + Keychain round-trip.
  3. feat(ai)ProviderType.chatGPT enum case, loopback listener,
    OAuth service with refresh coalescing, ChatGPTProvider, factory /
    registry / DI wiring.
  4. feat(settings)ProviderEditSheet Sign-in / Sign-out UI,
    AIChatView send-button gating on token presence, SettingsView
    keychain cleanup on row removal.

Risk / caveats

  • Not officially supported by OpenAI. This reuses the Codex CLI's public
    OAuth client_id. If OpenAI rotates that client or changes the
    /backend-api/codex/* shape, the provider breaks until the constants in
    ChatGPTOAuthConstants.swift are updated.
  • Requires a paid ChatGPT plan. Free-tier accounts cannot reach
    /backend-api/codex/responses.
  • macOS-only. The loopback listener uses Network.framework;
    Windows/Linux ports would need their own implementation.
  • No long-lived API key. Sign-out (manual or 401-triggered) deletes the
    Keychain bundle; users have to re-authenticate.

Tests

  • ChatGPTOAuthTests.swift — PKCE / JWT / token-bundle codec / Keychain
    round-trip
  • ChatGPTOAuthServiceTests.swift — URL-protocol-mocked refresh path,
    concurrent-refresh coalescing, expired-token short-circuit,
    missing-keychain throw, SSE happy path + error event, /models filter +
    401 sign-out
  • 28 / 28 passing locally (swift test --filter ChatGPTOAuth, ~0.5 s)

UI (sheet state machine, send-button gating) is covered by manual exploratory
testing — see test plan below. The codebase has no precedent for SwiftUI view
unit tests, so adding ViewInspector/XCUITest scaffolding solely for this PR
felt like over-engineering.

Test plan

  • swift test --filter ChatGPTOAuth — 28 / 28 green
  • swift build — no new warnings
  • ./scripts/build-app.shGridex.app v0.0.14 builds, ~77 MB,
    ad-hoc signed
  • Add Provider → pick "OpenAI (Sign in)" → Sign in → Save → AI chat works
  • Add Provider → Sign in → Cancel sheet → Keychain bundle cleaned up
    (security find-generic-password -s "Gridex" | grep ai.chatgpt.tokens
    returns nothing)
  • Edit existing ChatGPT row → Cancel sheet → bundle preserved
    (no rollback)
  • Edit a row → switch type from API-key to ChatGPT → Sign in →
    Cancel → bundle cleaned up
  • Sign out via UI → AIChatView send button greys out
  • Force a 401 (revoke token elsewhere) → next message clears bundle,
    UI reverts to signed-out
  • Remove provider row from Settings → both ai.apikey.<uuid> and
    ai.chatgpt.tokens.<uuid> purged

cyenxchen and others added 5 commits April 30, 2026 14:00
PKCE (RFC 7636) verifier/challenge generator and a minimal JWT payload
decoder, both used by the upcoming ChatGPT OAuth flow. Adds the codable
ChatGPTTokenBundle value type that wraps access/refresh/id tokens plus
account metadata.
Add saveChatGPTTokens / loadChatGPTTokens / deleteChatGPTTokens helpers
on KeychainService, keyed by provider UUID. Includes unit tests for the
PKCE/JWT/TokenBundle primitives and the new Keychain round-trip.
Wire the Codex CLI OAuth flow end-to-end: ProviderType.chatGPT enum case,
loopback HTTP listener, OAuth service with token-refresh coalescing, and
ChatGPTProvider talking to chatgpt.com/backend-api/codex/responses with
SSE streaming. Factory/Registry/DependencyContainer pass the OAuth
service through; 401/403 from the backend purges the keychain bundle.
ProviderEditSheet shows Sign in / Sign out buttons for chatGPT-type
providers, AIChatView gates send-button when no token bundle is present,
SettingsView purges the bundle when the provider row is removed.
Three review fixes for PR gridex#53.

1. **`defaultModel = "gpt-5.4"` → `gpt-5-codex`** (Core/Enums/ProviderType.swift)
   `gpt-5.4` is not a real OpenAI model slug. A user creating a fresh
   ChatGPT provider with `model: nil` would default to it, then fire a
   request before the live `/backend-api/codex/models` picker resolved —
   producing an immediate HTTP 400 from `/responses`. Switched to
   `gpt-5-codex`, the slug Codex CLI itself defaults to.

2. **`preconditionFailure` → in-band `GridexError`** (Services/AI/ProviderFactory.swift)
   Crashing the process when `chatGPTOAuthService` is nil for a
   `.chatGPT` config violates the project's "errors are GridexError,
   never raw crashes" invariant (AGENTS.md §0). The legacy
   `make(type:apiKey:baseURL:)` overload can reach this branch since it
   never threads the OAuth service through. Replaced with a private
   `MisconfiguredChatGPTProvider` stub that throws
   `GridexError.aiProviderError("ChatGPT provider misconfigured…")` from
   `stream()` / `availableModels()` / `validateAPIKey()`. The misconfig
   surfaces as a normal in-band error on first request rather than a
   process-wide crash.

3. **Cancel OAuth task on type-switch out of `.chatGPT`** (ProviderEditSheet.swift)
   `applyTypeDefaults` only reset state when switching INTO `.chatGPT`.
   Switching OUT (Add → pick ChatGPT → Sign in → browser slow → user
   changes mind → switch type back to OpenAI) left the in-flight OAuth
   task alive, with `isSigningIn` / `signInError` / `chatGPTStatus` /
   `dirtyChatGPTSignIn` stale. A tardy callback could still write
   tokens to `ai.chatgpt.tokens.<uuid>` after the form had moved on.
   The keychain converged via the Save / Cancel paths but the user-
   visible state was incoherent. Now the non-`.chatGPT` branch cancels
   the task and resets the four state flags.

Tests
- New `ProviderFactoryTests` (6 cases) covering the dispatch matrix:
  `.chatGPT` with OAuth service → `ChatGPTProvider`; `.chatGPT` without
  → `MisconfiguredChatGPTProvider` (stream/availableModels both throw
  `GridexError.aiProviderError`); `.anthropic` → `AnthropicProvider`;
  `.openAI` → `OpenAIProvider`; `defaultModel` regression assert
  pinning the literal `"gpt-5.4"` out and a known-real `gpt-5*` /
  `gpt-4o` slug in.
- Existing 28 OAuth tests still pass.
- 34 / 34 in the AI/auth families green; full suite green after a
  routine 1-day TLS cert renewal on the local PG :55435 fixture
  (unrelated to this PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinhntq
Copy link
Copy Markdown
Contributor

Pushed c9eb0f4 to your branch with three review fixes plus dispatch coverage. Security review came back clean (no high-confidence findings on PKCE / state / loopback bind / Keychain / JWT / token transmission), and code-quality review surfaced these three. maintainerCanModify was on, hence the direct push.

What changed

1. defaultModel was an invented slug — would 400 on first request

ProviderType.swift:107 had case .chatGPT: return "gpt-5.4". That slug doesn't exist server-side, and the surrounding comment at line 171–174 explicitly says ChatGPT slugs come from the live /backend-api/codex/models fetch — the default contradicted that.

Repro: create a ChatGPT provider, sign in, fire the first request before the live models picker resolves → model: nil defaults to gpt-5.4 → HTTP 400 from /responses.

Switched to gpt-5-codex, which is what Codex CLI itself defaults to.

2. preconditionFailure violated the GridexError invariant

ProviderFactory.swift:35 crashed the process when chatGPTOAuthService was nil for a .chatGPT config. Reachable via the legacy make(type:apiKey:baseURL:) overload (used by DependencyContainer.makeLLMService at DependencyContainer.swift:119) — that overload can't thread an OAuth service through.

Replaced with a private MisconfiguredChatGPTProvider stub. Misconfig now surfaces as a normal in-band GridexError.aiProviderError(...) on the first call rather than a process-wide crash. Matches the AGENTS.md §0 invariant: "Errors are GridexError. Never throw raw NSError or crash."

3. Type-switch out of .chatGPT left a zombie OAuth task

ProviderEditSheet.applyTypeDefaults only reset state when switching INTO .chatGPT. Scenario: user opens Add Provider → picks ChatGPT → taps Sign in with OpenAI → browser is slow → user changes their mind → switches Type back to OpenAI. The in-flight OAuth task kept running with stale isSigningIn / signInError / chatGPTStatus / dirtyChatGPTSignIn, and a tardy browser callback could still write tokens to ai.chatgpt.tokens.<uuid> after the form had moved on.

The keychain still converged via the Save/Cancel cleanup paths, but the user-visible state was incoherent. Now the non-.chatGPT branch cancels the task and resets the four flags.

Tests

Added ProviderFactoryTests.swift (6 cases) covering the dispatch matrix:

  • .chatGPT with OAuth service → ChatGPTProvider
  • .chatGPT without OAuth service → MisconfiguredChatGPTProvider whose stream() and availableModels() both throw GridexError.aiProviderError
  • .anthropicAnthropicProvider, .openAIOpenAIProvider
  • Regression assert pinning the literal "gpt-5.4" out and a gpt-5* / gpt-4o slug in

Result: 34 / 34 AI tests pass (28 existing OAuth + 6 new factory). Full suite also green after a routine TLS-cert renewal on the local PG :55435 fixture (1-day expiry on a self-signed cert — unrelated to this PR).

swift test --filter "ChatGPTOAuth|ProviderFactoryTests"
…
Executed 34 tests, with 0 failures (0 unexpected) in 0.392s

Still open (PR description's own checkboxes)

The four manual UI items in your test plan are unchecked — these now matter more, especially the last two:

  • Add Provider → pick ChatGPT → Sign in → Save → AI chat works
  • Add Provider → Sign in → Cancel sheet → Keychain bundle cleaned up
  • Edit existing ChatGPT row → Cancel sheet → bundle preserved
  • Edit a row → switch type from API-key to ChatGPT → Sign in → Cancel → bundle cleaned up ← this is exactly the zombie-state path fix (3) addresses; please verify

Suggested but not pushed (your call)

  • ChatGPTProvider.swift:81, :254 — error-body logged with privacy: .public. Server-controlled body can echo prompt content; consider .private for symmetry with the request-side logging at line 202–205.
  • ChatGPTProvider.swift:67-71 — silent signOut on every 401/403. A transient 401 wipes the user's tokens and forces a full re-sign-in. Codex CLI's own behavior is closer to "refresh first, signOut only on invalid_grant". A one-line clarifying comment ("by the time we hit stream(), tokenBundle() has already refreshed; a 401 here means the new token was rejected → genuine signOut") would also work if you'd rather keep the current shape.

ToS / Compliance — non-code, but worth surfacing

Reusing the public Codex CLI client_id to hit chatgpt.com/backend-api/codex/responses is a gray area against OpenAI's terms. The author's own caveats already note "OpenAI may revoke at any time" and "/backend-api/codex shape may change" — it'd be worth a small "Unofficial — uses Codex CLI OAuth client; may break or violate ToS, use at your own risk" disclaimer in the Settings sheet next to the Sign in button. Not a code blocker; just protects users (account suspension risk) and the project (C&D risk). Happy to add the disclaimer in a follow-up if you'd like.

From my side

With the three fixes pushed, this is good to merge once you've ticked the four manual UI items above, particularly the type-switch flow. Excellent work overall — security review was textbook clean and the 4-commit split is bisect-friendly.

Maintainer confirmed against the live `/backend-api/codex/responses`
endpoint that `gpt-5.4` is in fact a valid OpenAI model slug. My
earlier guess that it was an invented decimal-style ID was wrong —
I had no realtime access to OpenAI's current model catalog and
shouldn't have assumed.

Restore the original `gpt-5.4` default and drop the regression
assertion in ProviderFactoryTests that pinned it out. Keep the
non-empty assertion since `defaultModel` must hold *some* value for
the form to send before the live `/models` picker resolves.

The other two fixes from c9eb0f4 stand:
  - `MisconfiguredChatGPTProvider` stub instead of `preconditionFailure`
  - applyTypeDefaults now cancels the OAuth task on type-switch out

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinhntq thinhntq merged commit 518c6c5 into gridex:main Apr 30, 2026
1 check passed
haidang1810 added a commit that referenced this pull request May 1, 2026
Adds ChatGPT subscription provider via the Codex CLI's public OAuth
client. Settings → Provider → 'OpenAI (Sign in)' opens the system
browser, runs the PKCE flow against auth.openai.com, captures the
redirect on a loopback HTTP listener, exchanges the code for tokens,
and stores them in a DPAPI-encrypted file under %APPDATA%\Gridex.
Subsequent chat calls use the bearer token against
chatgpt.com/backend-api/codex/responses with SSE streaming.

Files added:
- Models/ChatGPTTokenBundle.h            token struct + JSON helpers
- Services/ChatGPTOAuth/PKCE.{h,cpp}     verifier/challenge via BCryptGenRandom + EVP_sha256 (base64url)
- Services/ChatGPTOAuth/JwtDecoder.{h,cpp}   base64url-decode payload, parse JSON, extract email + chatgpt_account_id + exp
- Services/ChatGPTOAuth/ChatGPTOAuthConstants.h  client_id, scopes, port 1455, refresh skew
- Services/ChatGPTOAuth/ChatGPTOAuthService.{h,cpp}  singleton: SignIn (browser + httplib::Server callback), refresh coalesced via mutex+shared_future, BearerToken/AccountId/CurrentEmail accessors

AiService:
- AiProvider::ChatGPT enum case (index 5)
- CallChatGPT — WinHTTP POST /backend-api/codex/responses, parses SSE
  delta events into accumulated text. 401/403 wipes tokens.
- FetchModels ChatGPT case — WinHTTP GET /backend-api/codex/models
  ?client_version=1.0.0 with Bearer + ChatGPT-Account-ID. Filters to
  models marked supported_in_api=true and visibility=list.

SettingsPage:
- New 'OpenAI (Sign in)' ComboBoxItem.
- ApiKeyPanel hides for ChatGPT; SignInPanel (status + Sign in/out)
  takes its place. OllamaEndpointPanel hides unless provider=Ollama.
- Sign-in success auto-clears Model dropdown and fetches the live
  list off /backend-api/codex/models so the previous provider's
  model name doesn't leak into the new session.

Why WinHTTP for ChatGPT only:
- Vcpkg cpp-httplib 0.40.0 + OpenSSL 3.x DLL has an ABI / threading
  mismatch — httplib::SSLClient destructor calls SSL_shutdown with
  a corrupted handle (s = 0x2) on the worker thread, fast-failing
  the process.
- Other providers (Anthropic / OpenAI / Ollama / Gemini / OpenRouter)
  still use httplib::Client because httplib auto-creates an
  SSLClient internally for https URLs and that path doesn't crash
  in practice for those hosts. Could migrate them later if needed.

Deviations from mac:
- Single token file under %APPDATA%\Gridex\chatgpt-tokens.bin
  (no per-provider UUID — Windows settings model is single-active).
- Loopback listener uses cpp-httplib::Server in plain HTTP mode
  rather than mac's Network.framework + bespoke parser.
- redirect_uri uses 'localhost' (not '127.0.0.1') because the
  registered Codex CLI client_id only accepts the literal hostname.
- Authorize URL + token-exchange body percent-encoded explicitly;
  the scope string contains spaces and reserved chars in
  redirect_uri.
haidang1810 added a commit that referenced this pull request May 1, 2026
* feat(grid): right-click 'Copy cell' menu copies full untruncated value

Cells visually truncate at MAX_CELL_DISPLAY_CHARS, so a TextBlock-
level Ctrl+C only saw the visible prefix. Added a 'Copy cell' item
at the top of the data-grid context menu that pulls the FULL value
from data_.rows directly — works regardless of how aggressively the
cell is clipped for layout.

Wire-up:
- Each cell TextBlock now stamps Tag(int32 colIndex).
- RightTapped on the row walks OriginalSource up the visual tree to
  find the cell TextBlock and stash its column in contextCol_.
- Menu Opening gates the item on (selectedRow_ >= 0 && contextCol_ >= 0).
- CopyCellToClipboard reads data_.rows[selectedRow_][colName] and
  ships a DataPackage with the full string. NULL sentinel renders
  as the literal 'NULL' so paste targets see something.

* chore(release): bump windows version to 0.1.10

* feat(connection): persist environment tag + show coloured badge on cards

The TagCombo dropdown (None/Production/Staging/Development/Testing/
Local) was rendered in ConnectionFormDialog.xaml but never wired —
selection didn't round-trip on Save / Edit, and nothing in the UI
reflected the tag.

End-to-end fix:

- ConnectionConfig: add 'tag' field (free-form wstring; empty = None
  so callers can ignore it cleanly).
- ConnectionStore: ALTER TABLE adds 'tag TEXT DEFAULT ""'; SELECT
  reads col 25, INSERT binds placeholder 24. Legacy rows migrate to
  empty string.
- ConnectionFormDialog: Save reads TagCombo.SelectedIndex (None=0
  stays empty) and pulls ComboBoxItem.Content. SetConnectionConfig
  matches the saved tag against the option list to restore selection.
- ConnectionCard: new EnvTagBadge next to the name with a per-tag
  colour (Production red / Staging orange / Development green /
  Testing purple / Local slate), Collapsed when tag is empty.

* feat(ai): port 'OpenAI (Sign in)' OAuth provider from mac PR #53

Adds ChatGPT subscription provider via the Codex CLI's public OAuth
client. Settings → Provider → 'OpenAI (Sign in)' opens the system
browser, runs the PKCE flow against auth.openai.com, captures the
redirect on a loopback HTTP listener, exchanges the code for tokens,
and stores them in a DPAPI-encrypted file under %APPDATA%\Gridex.
Subsequent chat calls use the bearer token against
chatgpt.com/backend-api/codex/responses with SSE streaming.

Files added:
- Models/ChatGPTTokenBundle.h            token struct + JSON helpers
- Services/ChatGPTOAuth/PKCE.{h,cpp}     verifier/challenge via BCryptGenRandom + EVP_sha256 (base64url)
- Services/ChatGPTOAuth/JwtDecoder.{h,cpp}   base64url-decode payload, parse JSON, extract email + chatgpt_account_id + exp
- Services/ChatGPTOAuth/ChatGPTOAuthConstants.h  client_id, scopes, port 1455, refresh skew
- Services/ChatGPTOAuth/ChatGPTOAuthService.{h,cpp}  singleton: SignIn (browser + httplib::Server callback), refresh coalesced via mutex+shared_future, BearerToken/AccountId/CurrentEmail accessors

AiService:
- AiProvider::ChatGPT enum case (index 5)
- CallChatGPT — WinHTTP POST /backend-api/codex/responses, parses SSE
  delta events into accumulated text. 401/403 wipes tokens.
- FetchModels ChatGPT case — WinHTTP GET /backend-api/codex/models
  ?client_version=1.0.0 with Bearer + ChatGPT-Account-ID. Filters to
  models marked supported_in_api=true and visibility=list.

SettingsPage:
- New 'OpenAI (Sign in)' ComboBoxItem.
- ApiKeyPanel hides for ChatGPT; SignInPanel (status + Sign in/out)
  takes its place. OllamaEndpointPanel hides unless provider=Ollama.
- Sign-in success auto-clears Model dropdown and fetches the live
  list off /backend-api/codex/models so the previous provider's
  model name doesn't leak into the new session.

Why WinHTTP for ChatGPT only:
- Vcpkg cpp-httplib 0.40.0 + OpenSSL 3.x DLL has an ABI / threading
  mismatch — httplib::SSLClient destructor calls SSL_shutdown with
  a corrupted handle (s = 0x2) on the worker thread, fast-failing
  the process.
- Other providers (Anthropic / OpenAI / Ollama / Gemini / OpenRouter)
  still use httplib::Client because httplib auto-creates an
  SSLClient internally for https URLs and that path doesn't crash
  in practice for those hosts. Could migrate them later if needed.

Deviations from mac:
- Single token file under %APPDATA%\Gridex\chatgpt-tokens.bin
  (no per-provider UUID — Windows settings model is single-active).
- Loopback listener uses cpp-httplib::Server in plain HTTP mode
  rather than mac's Network.framework + bespoke parser.
- redirect_uri uses 'localhost' (not '127.0.0.1') because the
  registered Codex CLI client_id only accepts the literal hostname.
- Authorize URL + token-exchange body percent-encoded explicitly;
  the scope string contains spaces and reserved chars in
  redirect_uri.
haidang1810 added a commit that referenced this pull request Jun 2, 2026
…ton (#68)

* feat(grid): right-click 'Copy cell' menu copies full untruncated value

Cells visually truncate at MAX_CELL_DISPLAY_CHARS, so a TextBlock-
level Ctrl+C only saw the visible prefix. Added a 'Copy cell' item
at the top of the data-grid context menu that pulls the FULL value
from data_.rows directly — works regardless of how aggressively the
cell is clipped for layout.

Wire-up:
- Each cell TextBlock now stamps Tag(int32 colIndex).
- RightTapped on the row walks OriginalSource up the visual tree to
  find the cell TextBlock and stash its column in contextCol_.
- Menu Opening gates the item on (selectedRow_ >= 0 && contextCol_ >= 0).
- CopyCellToClipboard reads data_.rows[selectedRow_][colName] and
  ships a DataPackage with the full string. NULL sentinel renders
  as the literal 'NULL' so paste targets see something.

* chore(release): bump windows version to 0.1.10

* feat(connection): persist environment tag + show coloured badge on cards

The TagCombo dropdown (None/Production/Staging/Development/Testing/
Local) was rendered in ConnectionFormDialog.xaml but never wired —
selection didn't round-trip on Save / Edit, and nothing in the UI
reflected the tag.

End-to-end fix:

- ConnectionConfig: add 'tag' field (free-form wstring; empty = None
  so callers can ignore it cleanly).
- ConnectionStore: ALTER TABLE adds 'tag TEXT DEFAULT ""'; SELECT
  reads col 25, INSERT binds placeholder 24. Legacy rows migrate to
  empty string.
- ConnectionFormDialog: Save reads TagCombo.SelectedIndex (None=0
  stays empty) and pulls ComboBoxItem.Content. SetConnectionConfig
  matches the saved tag against the option list to restore selection.
- ConnectionCard: new EnvTagBadge next to the name with a per-tag
  colour (Production red / Staging orange / Development green /
  Testing purple / Local slate), Collapsed when tag is empty.

* feat(ai): port 'OpenAI (Sign in)' OAuth provider from mac PR #53

Adds ChatGPT subscription provider via the Codex CLI's public OAuth
client. Settings → Provider → 'OpenAI (Sign in)' opens the system
browser, runs the PKCE flow against auth.openai.com, captures the
redirect on a loopback HTTP listener, exchanges the code for tokens,
and stores them in a DPAPI-encrypted file under %APPDATA%\Gridex.
Subsequent chat calls use the bearer token against
chatgpt.com/backend-api/codex/responses with SSE streaming.

Files added:
- Models/ChatGPTTokenBundle.h            token struct + JSON helpers
- Services/ChatGPTOAuth/PKCE.{h,cpp}     verifier/challenge via BCryptGenRandom + EVP_sha256 (base64url)
- Services/ChatGPTOAuth/JwtDecoder.{h,cpp}   base64url-decode payload, parse JSON, extract email + chatgpt_account_id + exp
- Services/ChatGPTOAuth/ChatGPTOAuthConstants.h  client_id, scopes, port 1455, refresh skew
- Services/ChatGPTOAuth/ChatGPTOAuthService.{h,cpp}  singleton: SignIn (browser + httplib::Server callback), refresh coalesced via mutex+shared_future, BearerToken/AccountId/CurrentEmail accessors

AiService:
- AiProvider::ChatGPT enum case (index 5)
- CallChatGPT — WinHTTP POST /backend-api/codex/responses, parses SSE
  delta events into accumulated text. 401/403 wipes tokens.
- FetchModels ChatGPT case — WinHTTP GET /backend-api/codex/models
  ?client_version=1.0.0 with Bearer + ChatGPT-Account-ID. Filters to
  models marked supported_in_api=true and visibility=list.

SettingsPage:
- New 'OpenAI (Sign in)' ComboBoxItem.
- ApiKeyPanel hides for ChatGPT; SignInPanel (status + Sign in/out)
  takes its place. OllamaEndpointPanel hides unless provider=Ollama.
- Sign-in success auto-clears Model dropdown and fetches the live
  list off /backend-api/codex/models so the previous provider's
  model name doesn't leak into the new session.

Why WinHTTP for ChatGPT only:
- Vcpkg cpp-httplib 0.40.0 + OpenSSL 3.x DLL has an ABI / threading
  mismatch — httplib::SSLClient destructor calls SSL_shutdown with
  a corrupted handle (s = 0x2) on the worker thread, fast-failing
  the process.
- Other providers (Anthropic / OpenAI / Ollama / Gemini / OpenRouter)
  still use httplib::Client because httplib auto-creates an
  SSLClient internally for https URLs and that path doesn't crash
  in practice for those hosts. Could migrate them later if needed.

Deviations from mac:
- Single token file under %APPDATA%\Gridex\chatgpt-tokens.bin
  (no per-provider UUID — Windows settings model is single-active).
- Loopback listener uses cpp-httplib::Server in plain HTTP mode
  rather than mac's Network.framework + bespoke parser.
- redirect_uri uses 'localhost' (not '127.0.0.1') because the
  registered Codex CLI client_id only accepts the literal hostname.
- Authorize URL + token-exchange body percent-encoded explicitly;
  the scope string contains spaces and reserved chars in
  redirect_uri.

* chore(release): bump windows version to 0.1.11

* feat(workspace): Create Database dialog + scrollable picker with sticky New button

Three things shipped together:

1. Create-Database flow. ShowCreateDatabaseDialogAsync presents a
   ContentDialog with a name TextBox, executes
   CREATE DATABASE "<name>" (or backtick-quoted for MySQL/CH) on
   the active adapter, refreshes the picker, and switches to the new
   database. SQLite + Redis hide the entry entirely (engines without
   DB-level DDL).

2. Scrollable database picker. Replaced MenuFlyout with a custom
   Flyout: ScrollViewer + StackPanel holds the database list capped
   at MaxHeight=380, so connections with 30+ databases no longer
   overflow the screen. Each DB renders as a flat Button styled like
   a menu item (Flyout doesn't accept MenuFlyoutItem children).

3. Sticky New-database button. Lives in row 1 of the picker grid,
   outside the scroll region — always visible at the bottom no
   matter how far the user scrolls through the database list. Click
   handler wired once in init; LoadDatabasePicker only flips
   visibility based on engine support.

Bonus: ellipsis literal moved to \u2026 escape so it survives the
non-UTF-8-BOM source file.

* chore(release): bump windows version to 0.1.12

* fix(windows): stabilize mongo connect and libssh2 deploy (#77)
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.

2 participants