feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001
feat: [AI-7520] Altimate LLM Gateway CLI signup + onboarding UX#1001saravmajestic wants to merge 10 commits into
Conversation
- Add an `oauth` `method:"auto"` to the Altimate auth plugin: bind a loopback server on `localhost:7317`, open the browser to the web authorize page, verify `state`, and save the gateway credential to `~/.altimate/altimate.json` - Surface `altimate-backend` first in provider selection (TUI + clack) with the "Recommended · best tool-calling · 10M free tokens" hint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uth success
- Add `WelcomePanel` boot box (readiness-aware tips + "What is Altimate Code")
on the home screen; first run is a welcoming panel, not an auto-opened modal
- Restructure the model picker into READY / NEEDS-SETUP; add the curated
`DialogModelWelcome` (top 5 providers + "Search all providers"), Big Pickle
fallback, and `useReady` / `markSetupComplete`
- `/connect` opens the curated picker
- Altimate LLM Gateway sign-in confirms inline ("Authentication successful") and
auto-closes (auto-selecting a model) instead of dropping into the model picker
- Don't force Google: land on the sign-up page and let the user choose
(drop `google_start`); reword the sign-in instruction
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
- /auth: sign in to the Altimate LLM Gateway directly via the OAuth loopback, skipping the provider picker (new `DialogAltimateAuth`) - /logout: clear the stored gateway credential (`AltimateApi.clearCredentials`) and disconnect (dispose + bootstrap; the provider loader drops the now-stale auth-store entry on reload) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DialogModelWelcome now tags each row with its providerID (and modelID for Big Pickle) and compares against local.model.current(), rendering a bright-green ✓ plus a "· selected" note on the active provider — so the user can see at a glance that e.g. Altimate LLM Gateway is already selected. Bright green (diffHighlightAdded) is used because plain ANSI green renders dim in some terminals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ebase safety) Shrink our footprint on the upstream opencode `dialog-model.tsx` so future upstream merges are easier to rebase: - Move `useReady`/`markSetupComplete`, `DialogModelWelcome`, and `DialogBigPickleConfirm` into a new altimate-owned `component/altimate-onboarding.tsx` (zero rebase surface — our file) - `dialog-model.tsx` (424 → 160 lines) now holds only pristine `useConnected` plus the `DialogModel` READY/NEEDS-SETUP restructure, fully wrapped in `altimate_change` markers so an upstream conflict is confined and human-resolvable - Repoint imports in `app.tsx`, `home.tsx`, `dialog-provider.tsx`, `welcome-panel.tsx` The onboarding file imports `DialogModel`/`useConnected` back from `dialog-model`, but only inside callbacks/JSX — the circular reference is runtime-only and safe. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sahrizvi
left a comment
There was a problem hiding this comment.
Consensus code review — PR #1001
Independent reviews from 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and then re-validated line-by-line against the checked-out code.
Verdict: changes requested. The onboarding refactor and OAuth loopback design are well-structured (crypto-random state, CSRF state check, 0o600 credential file, cleanly isolated onboarding). But the new loopback auth server has correctness/reliability bugs and localhost security-hardening gaps — 7 major, 9 minor — all in the auth path. See inline comments.
Nits (not inlined): recent-models section dropped from the model picker; Big Pickle won't show a ✓ in the full picker after selection; error page doesn't window.close() like the success page; documented circular import between altimate-onboarding.tsx and dialog-model.tsx; silent open() failure (mitigated — the URL is still rendered); no ALTIMATE_CALLBACK_PORT override.
Positives: crypto-random state + CSRF check, 0o600 credentials, clean rebase-safe isolation of onboarding, reuse of existing provider onSelect handlers, reliable server teardown on the normal path.
Missing tests: the entire OAuth loopback flow (early callback, invalid/missing state, error branch, timeout, port collision, persistence) and the /auth · /logout · Big Pickle · mixed-row picker paths are untested.
| // Bind the port BEFORE opening the browser so the credential can | ||
| // only be delivered to this process. | ||
| const state = randomBytes(16).toString("hex") | ||
| await startCallbackServer() |
There was a problem hiding this comment.
The browser can hand back the login before we're listening for it
MAJOR — Race — pending is registered too late. startCallbackServer() binds the port and starts accepting requests here inside authorize(), but the module-level pending (state + resolve/reject) is only set later when the framework calls the returned callback() (waitForCallback, ~L114). If the browser redirects before callback() runs (an already-signed-in user gets an instant redirect, or any scheduling gap), /callback sees pending === undefined and rejects as "Invalid state — possible CSRF", silently dropping the credential. The singleton also can't support two concurrent /auth flows — the second overwrites the first.
Fix: register pending keyed by state (e.g. a Map) synchronously in authorize() before opening the browser; have callback() await that promise, and buffer an early callback by state until it's awaited.
There was a problem hiding this comment.
Fixed in e46fdd48b. registerPending(state) now runs synchronously inside authorize() before open(), keying the pending flow by the unguessable state. The /callback handler resolves/rejects by that state, so an instant redirect (already-signed-in user) is matched instead of dropped as CSRF. See altimate.ts:162-171.
| : undefined | ||
| if (model) local.model.set({ providerID: props.providerID, modelID: model }, { recent: true }) | ||
| setConnected(true) | ||
| setTimeout(() => dialog.clear(), 5000) |
There was a problem hiding this comment.
Backing out of the sign-in screen doesn't actually stop the sign-in
MAJOR — No cancellation/cleanup when the dialog is dismissed. Pressing esc (or unmounting) only calls dialog.clear(); it never cancels the pending callback, so the loopback server can stay open for ~5 minutes and a late browser callback can still mutate app state after dismissal. This success-path setTimeout(() => dialog.clear(), 5000) is also never retained or cleared on unmount, so it can fire later and dismiss an unrelated dialog opened in the meantime.
Fix: guard post-await UI updates with an onCleanup disposed flag, clearTimeout in onCleanup, and stop the callback server / abort the SDK callback when the dialog is dismissed.
There was a problem hiding this comment.
Addressed in f5596b40a (UI leak) + 40c80361c. AutoMethod now has an onCleanup/disposed guard and the success setTimeout is retained and cleared on unmount, so a late callback can no longer dismiss an unrelated dialog. The server-side half (eagerly cancelling the pending flow on dismiss) is tracked as NEW-3 below — the dismiss path can't reach the loopback pending map, so the 5-min timeout + callback()'s finally (stops the server when it's the last flow) is the backstop.
| let pending: Pending | undefined | ||
|
|
||
| async function startCallbackServer(): Promise<void> { | ||
| if (server) return |
There was a problem hiding this comment.
If port 7317 is busy, sign-in silently hangs forever with no retry
MAJOR — Port collision leaves a broken, silently-failing server. server is assigned by createServer() before listen(). On EADDRINUSE the listen promise rejects (L94-96) but server is never reset to undefined, so this if (server) return early-returns on the next attempt and the flow proceeds as if listening — the callback never arrives and the user waits out the 5-minute timeout with no message.
Fix: in the error handler set server = undefined (and close it), and surface a clear "port 7317 in use" error to the TUI. Consider an ephemeral port if the redirect contract allows.
There was a problem hiding this comment.
Fixed in e46fdd48b. On listen error we now reset server = undefined (clearing the if (server) return guard so a retry can re-bind) and throw an EADDRINUSE-specific message instead of hanging: "Port 7317 is already in use — close whatever is using it and try again." See altimate.ts:112-122.
| <h2>Signed in ✓</h2><p>You can return to your terminal.</p> | ||
| <script>setTimeout(()=>window.close(),1500)</script></body>` | ||
|
|
||
| const HTML_ERROR = (msg: string) => `<!doctype html><meta charset="utf-8"><title>Altimate Code</title> |
There was a problem hiding this comment.
The error page echoes attacker-controlled text straight into HTML
MAJOR — Reflected, unescaped input in the callback error page (localhost XSS). HTML_ERROR(msg) interpolates msg into HTML with no escaping, and the value ultimately comes from url.searchParams (the error branch at L57, which runs before the state check). Any page that drives the browser to http://localhost:7317/callback?error=<script>... during the auth window yields reflected XSS on the localhost origin. Impact is limited (transient page, no secrets/cookies there) but it's avoidable.
Fix: HTML-escape all reflected values, and validate state before handling the error branch.
There was a problem hiding this comment.
Fixed in e46fdd48b. Added escapeHtml() and applied it to the reflected value in HTML_ERROR (altimate.ts:20-25,37). The related root cause is also closed — state is now validated before the ?error= branch runs (see the abort-path comment), so untrusted input no longer reaches that branch without a valid state.
| }) | ||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| server!.listen(CALLBACK_PORT, () => resolve()) |
There was a problem hiding this comment.
The sign-in callback server is reachable from the whole network, not just this machine
MAJOR — Callback server listens on all interfaces, not loopback. listen(CALLBACK_PORT) with no host binds to ::/0.0.0.0, so the auth callback server is reachable from the LAN — despite the comment claiming "the credential can only be delivered to this process." The 128-bit state still protects credential delivery, but the listener still processes remote requests (probing, plus the pre-state-check error abort below).
Fix: server!.listen(CALLBACK_PORT, "127.0.0.1", () => resolve()).
There was a problem hiding this comment.
Fixed in e46fdd48b. server.listen(CALLBACK_PORT, "127.0.0.1", ...) now binds loopback only, so the callback server is unreachable from the LAN. altimate.ts:109.
| // A provider is "ready" (usable now) when it has valid credentials: it is present | ||
| // in the live provider list with at least one model — and, for the free OpenCode | ||
| // provider, with at least one paid model (a Zen key entered). | ||
| function providerReady(id: string) { |
There was a problem hiding this comment.
A provider with unknown pricing can be treated as "ready" without a key
MINOR — providerReady("opencode") mishandles undefined cost. m.cost?.input !== 0 evaluates to true when cost is undefined, so OpenCode can be counted as "ready" (usable now) even without a Zen key entered.
Fix: m.cost?.input != null && m.cost.input !== 0. (Same defect exists in useConnected() — see the separate comment.)
There was a problem hiding this comment.
Fixed in f5596b40a. providerReady("opencode") now uses m.cost?.input != null && m.cost.input !== 0, so undefined cost is no longer treated as ready. The matching fix in useConnected() is in the same commit.
| // Persist to ~/.altimate/altimate.json — the provider loader | ||
| // reads this first (it carries the instance/tenant + api_url | ||
| // the generic auth.json store can't). | ||
| await AltimateApi.saveCredentials({ |
There was a problem hiding this comment.
Browser sign-in skips the tenant-name check that manual entry enforces
MINOR — OAuth callback skips the tenant-name validation the manual path enforces. The manual paste path runs validateCredentials() (tenant regex + API check) before saving; this OAuth path calls saveCredentials() directly on the callback instance. The source is the trusted web app, so risk is low, but the invariant differs between the two entry points.
Fix: validate instance against VALID_TENANT_REGEX before persisting, or document why the OAuth source is trusted.
There was a problem hiding this comment.
Fixed in f5596b40a. The OAuth path now validates AltimateApi.isValidInstanceName(creds.instance) against VALID_TENANT_REGEX before exchange/persist, matching the manual paste path's invariant. altimate.ts:195.
| // useConnected() (real credentials) via useReady(), it gates the first-run chat | ||
| // lock. Module-global so it is shared across the app and resets on every process | ||
| // launch (so a fresh relaunch is a clean fresh-user state). | ||
| const [setupComplete, setSetupComplete] = createSignal(false) |
There was a problem hiding this comment.
After signing out, the app still shows "you're all set" guidance
MINOR — setupComplete is never reset on /logout. This module-global signal is OR'd with connected() in useReady(). /logout clears credentials (so connected() → false) but leaves setupComplete === true, so useReady() stays true for the rest of the process. Verified scope: useReady() only drives tips and the update-toast (not the chat/prompt), so the real impact is that the post-logout panel keeps showing "ready" tips (/discover) instead of /connect until relaunch. (Reviewers split on severity; kept minor given the cosmetic impact.)
Fix (if desired): gate the post-logout tips on connected() rather than useReady(), or add a resetSetupComplete() called from /logout.
There was a problem hiding this comment.
Fixed in 9c481ecf8. /logout now calls resetSetupComplete(), so setupComplete is cleared alongside credentials and useReady() returns to the /connect guidance without a relaunch.
| res.end(body) | ||
| } | ||
|
|
||
| const error = url.searchParams.get("error") |
There was a problem hiding this comment.
Anyone on the network can cancel your sign-in without any secret
MAJOR — Unauthenticated ?error= can abort an in-progress sign-in. This error branch runs before state validation and rejects/clears the current pending flow. Combined with the all-interface bind above, any local or LAN requester can cancel a user's sign-in without knowing state — there's no CSRF protection on the abort path.
Fix: validate state before honoring the error branch (this also closes the reflected-XSS issue on the same branch).
There was a problem hiding this comment.
Fixed in e46fdd48b. state is now validated first — before the ?error= branch. An unknown/missing state returns 400 and can neither cancel an in-progress flow nor deliver anything, so the abort path is no longer reachable without the 128-bit secret. altimate.ts:76-89.
| // are used by the restructured DialogModel below. | ||
| import { markSetupComplete, DialogBigPickleConfirm } from "./altimate-onboarding" | ||
|
|
||
| export function useConnected() { |
There was a problem hiding this comment.
The same unknown-pricing bug also mislabels providers as connected
MINOR — useConnected() has the same undefined-cost defect as providerReady(). It treats OpenCode as connected when any model has cost?.input !== 0, which is also true for undefined cost. This one has a wider blast radius: it drives the suggested: !connected() flag on the /connect and /auth commands, not just the model picker.
Fix: apply the same != null && !== 0 guard here.
There was a problem hiding this comment.
Fixed in f5596b40a. useConnected() now applies the same m.cost?.input != null && m.cost.input !== 0 guard, so undefined cost no longer marks OpenCode connected (and the suggested: !connected() flag on /connect//auth is correct).
sahrizvi
left a comment
There was a problem hiding this comment.
Requesting changes — PR #1001
Consensus review by 6 models (Claude, GPT 5.4 Codex, Kimi K2.5, MiniMax M2.7, Qwen 3.6, MiMo V2.5 Pro), converged and re-validated line-by-line against the code. Formalizing the verdict from the inline review above (#pullrequestreview-4705485161).
7 major, 9 minor findings — all in the new OAuth loopback auth path. The design is sound (crypto-random state, CSRF check, 0o600 credentials, cleanly isolated onboarding), but the loopback server has correctness/reliability bugs and localhost hardening gaps that should be fixed before merge.
Blocking (major):
- Race —
pendingregistered after the server starts accepting requests; an instant redirect drops the credential (altimate.ts:140). - Dismissing the dialog doesn't cancel the flow; server +
setTimeoutleak (dialog-provider.tsx:209). - Port collision (
EADDRINUSE) leavesserverset → silent 5-min hang on retry (altimate.ts:43). - Reflected, unescaped
errorin the callback HTML — localhost XSS (altimate.ts:23). - Callback server binds all interfaces instead of
127.0.0.1(altimate.ts:94). - API key delivered in the URL query string (
altimate.ts:76). - Unauthenticated
?error=aborts an in-progress sign-in without knowingstate(altimate.ts:57).
Minor findings and fixes are in the inline comments. Nothing here blocks the onboarding UX direction — the issues are contained to auth-path correctness, security hardening, and error handling. Happy to walk through any of them.
Address sahrizvi's review on #1001: - Register the pending flow in a state-keyed Map synchronously in authorize() BEFORE opening the browser, and have callback() await that promise — an already-signed-in user's instant redirect is no longer dropped as CSRF, and concurrent /auth flows no longer clobber each other - Bind the callback server to 127.0.0.1 (was all-interfaces / LAN-reachable) - Validate `state` BEFORE honoring the `?error=` branch, so an unauthenticated request can't cancel an in-progress sign-in - HTML-escape reflected values in the callback error page (localhost XSS) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- altimate.ts: reset the callback server on a failed listen (EADDRINUSE no longer wedges the singleton) + surface the reason; validate the callback instance name before persisting; log the failure reason on the auth catch path - dialog-provider: DialogAltimateAuth onMount wrapped in try/catch (no more stuck "Starting sign-in…"); AutoMethod guards post-await updates + clears its auto-close timer on unmount (onCleanup); on connect-with-no-model, open the picker instead of faking a green ✓ - dialog-model: guard the favorite keybind against string (NEEDS-SETUP) rows; treat undefined model cost as not-paid in providerReady() and useConnected() - app.tsx /logout: try/catch with error toast + reset the setupComplete flag - altimate-onboarding: add resetSetupComplete() Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oopback URL) The loopback now receives a short-lived `token` (login_token) instead of the raw key; callback() exchanges it via POST /auth/social/exchange (AltimateApi. exchangeSocialToken) for the auth_token and saves that. Single code path for both the Google and email/password connect flows. State validation, loopback bind, HTML escaping, and the pending-Map/one-time-server logic are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sahrizvi
left a comment
There was a problem hiding this comment.
Follow-up review — PR #1001 (still requesting changes)
Consensus re-review of the fix commits (e46fdd4, f5596b4, 7e7ac7c) by 6 models. All 16 prior findings are resolved (15 fully, #2 partially) — thank you. The fixes surfaced new issues, and one is a blocker, so keeping this at request changes.
Blocker (top priority): the OAuth callback's api_url is trusted with no scheme/host validation, and the CLI POSTs the one-time login_token to it (altimate.ts:90 → client.ts exchangeSocialToken). Unlike the manual-paste path, the OAuth path validates neither scheme nor host. A malicious browser extension or a web-app open-redirect could exfiltrate the token + tenant. Fix: require https:// and allowlist known Altimate hosts before exchanging (except in an explicit dev mode).
The five inline comments below (NEW-2 … NEW-6) are the remaining items — NEW-2 is a quick correctness fix worth doing now; the rest are fast-follows. Once the api_url validation and NEW-2 land, this is good to go.
| const result = registerPending(state) | ||
|
|
||
| const webUrl = (process.env.ALTIMATE_WEB_URL || DEFAULT_WEB_URL).replace(/\/+$/, "") | ||
| const redirect = `http://localhost:${CALLBACK_PORT}/callback` |
There was a problem hiding this comment.
The browser is sent to localhost but we only listen on IPv4 — sign-in can miss
MAJOR — NEW-2 — loopback binds 127.0.0.1 but the redirect uses http://localhost. altimate.ts:106 binds IPv4 loopback only, while the redirect here is http://localhost:${CALLBACK_PORT}/callback. On hosts where localhost resolves to ::1 first, the browser callback can hit a closed IPv6 port. Browser Happy-Eyeballs fallback often masks this, but it's fragile.
Fix: use http://127.0.0.1:${CALLBACK_PORT}/callback in the redirect to match the bind (or bind both loopback families).
Flagged by: GPT (MAJOR)
There was a problem hiding this comment.
Fixed in 40c80361c (NEW-2). The redirect is now http://127.0.0.1:${CALLBACK_PORT}/callback to match the IPv4 loopback bind, so a localhost→::1 resolution can't hit a closed port. altimate.ts:176.
| await startCallbackServer() | ||
| // Register the pending flow BEFORE opening the browser so an instant | ||
| // redirect can be matched by state rather than dropped as CSRF. | ||
| const result = registerPending(state) |
There was a problem hiding this comment.
Backing out of sign-in leaves the server and pending flow running for 5 minutes
MINOR — NEW-3 (residual of prior #2) — dismissing the dialog doesn't cancel the server-side flow. The onCleanup+disposed guard fixes the UI leak, but nothing rejects this eagerly-created registerPending promise on dismiss, so callback() keeps awaiting, the loopback server stays up, and the pending entry lingers until the 5-minute timeout.
Fix: on dismiss, reject/cancel the pending flow by state (and stop the server if it was the last one). Also attach a no-op .catch() to this registerPending promise so a never-awaited rejection can't become an unhandled rejection.
Flagged by: Kimi (MAJOR), MiMo, Claude — consensus
There was a problem hiding this comment.
Partially addressed in 40c80361c (NEW-3). Added void result.catch(() => {}) so the eagerly-created registerPending promise can't surface as an unhandled rejection when the dialog is dismissed. The eager reject-by-state on dismiss isn't feasible from here: the TUI dismiss path never receives state, so it can't reach the loopback pending map. The backstop remains the 5-min timeout (rejects the pending entry) and callback()'s finally (stops the server when it was the last flow). Happy to open a follow-up if we want true immediate cancel — it'd require threading state back out to the dialog.
| // Log the reason (CSRF / timeout / invalid instance / …). Runs in the | ||
| // server process, so this goes to the log, not the TUI display. | ||
| console.error("[altimate] gateway sign-in failed:", err instanceof Error ? err.message : err) | ||
| return { type: "failed" } |
There was a problem hiding this comment.
When sign-in fails, the user sees a generic error with no reason
MINOR — NEW-4 (UX) — auth/callback failures are opaque to the user. The plugin callback() logs the reason via console.error but returns bare { type: "failed" }; OauthCallbackFailed carries no message (auth-service.ts), so the TUI shows a generic failure with no reason (port busy, timeout, exchange 401, invalid instance).
Fix: thread a reason string through the failed result (or surface it via toast.error in the callback path).
Flagged by: MiniMax (rated CRITICAL), GPT, Kimi, MiMo — consensus
There was a problem hiding this comment.
Fixed in 40c80361c (NEW-4). AutoMethod now calls toast.error(...) on result.error instead of clearing silently, so port-busy/timeout/exchange-401/invalid-instance failures are visible in the TUI. The precise reason is still logged server-side via console.error.
| }[provider.id], | ||
| // altimate_change end | ||
| category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", | ||
| async onSelect() { |
There was a problem hiding this comment.
The recommended /connect path can still hang on a startup failure
MINOR — NEW-5 (UX) — /connect welcome path lacks the try/catch that /auth got. The onboarding fix wrapped DialogAltimateAuth, but the recommended /connect path routes through this createDialogProviderOptions().onSelect → oauth.authorize() (just below, ~L95) with no equivalent guard, so a port-collision/startup failure there still fails without the new toast.
Fix: wrap the OAuth authorize block in createDialogProviderOptions() with the same toast/clear behavior as DialogAltimateAuth.
Flagged by: GPT (MAJOR)
There was a problem hiding this comment.
Fixed in 40c80361c (NEW-5). The OAuth authorize block in createDialogProviderOptions() (the /connect path) is now wrapped in try/catch with the same toast.error(...) + dialog.clear() behavior as DialogAltimateAuth.
| } | ||
|
|
||
| entry.resolve({ api_url: apiUrl, instance, token }) | ||
| html(200, HTML_SUCCESS) |
There was a problem hiding this comment.
The browser says "Signed in" before we've actually finished signing in
MINOR — NEW-6 — browser shows "Signed in ✓" before the token exchange/persist succeeds. The loopback returns HTML_SUCCESS here as soon as it receives the token; if exchangeSocialToken()/save then fails, the browser says success while the terminal reports failure.
Fix: use neutral "authorization received, return to terminal" copy, or delay success until exchange + persist succeed.
Flagged by: GPT, MiniMax
There was a problem hiding this comment.
Fixed in 40c80361c (NEW-6). HTML_SUCCESS is now neutral — "Authorization received / Return to your terminal to finish connecting." — so the browser no longer claims success before the exchange + persist complete. The terminal remains the source of truth for actual success/failure.
- NEW-2: callback redirect uses http://127.0.0.1 to match the loopback bind (a plain `localhost` redirect can resolve to ::1 and hit a closed port) - NEW-6: neutral browser copy ("Authorization received") — the loopback replies before the CLI has exchanged/persisted, so it must not claim "Signed in" - NEW-5: guard the /connect OAuth authorize path with try/catch → toast + clear (parity with DialogAltimateAuth; port-collision no longer fails silently) - NEW-4: AutoMethod surfaces a toast on callback failure instead of clearing silently (the precise reason is logged server-side) - NEW-3: swallow a never-awaited pending rejection (void result.catch) to avoid an unhandled rejection when the dialog is dismissed before callback() runs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The welcome/top-5 picker already leads with the Altimate LLM Gateway. Order the full DialogModel list the same way: export `PROVIDER_PRIORITY` and sort the READY section's providers by it (the NEEDS-SETUP section already used it), so the gateway leads whether or not it is connected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
CLI onboarding for the Altimate LLM Gateway + a welcoming first-run experience, plus quick sign-in/out commands.
Gateway sign-in
:7317) + browser → web sign-up page → credential delivered back to the CLI; saved to~/.altimate/altimate.json(overridable viaALTIMATE_WEB_URL).First-run experience
WelcomePanelboot box on the home screen with readiness-aware tips (/connect→/discover); update toast suppressed until a model is ready.useReady/markSetupCompletegate the first-run chat lock (unlocks after connecting, choosing Big Pickle, or picking a ready model).Provider picker
/connectopens the curatedDialogModelWelcome— top 5 providers (Altimate Gateway first) + "Search all providers…", a Big Pickle fallback, and a bright-green ✓ "selected" marker on the currently-active provider/model./modeldialog is restructured into READY / NEEDS-SETUP sections.New TUI commands
/auth— start the Altimate LLM Gateway sign-in directly (DialogAltimateAuth)./logout— clear the saved gateway credential (AltimateApi.clearCredentials), disconnect, and reload.Rebase safety
component/altimate-onboarding.tsx;dialog-model.tsxtrimmed to pristineuseConnected+ the markedDialogModelrestructure, so future upstream (opencode) merges have a minimal, clearly-marked conflict surface. No behavior change.Test plan
/connect→ Altimate LLM Gateway → browser → provisioned + connected/agents/v1/models→200)/authlaunches gateway sign-in;/logoutclears creds + disconnectsbun run typecheckclean🤖 Generated with Claude Code