@@ -620,6 +620,10 @@ export class ClineProvider
620620 return findLast ( Array . from ( this . activeInstances ) , ( instance ) => instance . view ?. visible === true )
621621 }
622622
623+ public static getAllInstances ( ) : ClineProvider [ ] {
624+ return Array . from ( this . activeInstances )
625+ }
626+
623627 public static async getInstance ( ) : Promise < ClineProvider | undefined > {
624628 let visibleProvider = ClineProvider . getVisibleInstance ( )
625629
@@ -851,6 +855,64 @@ export class ClineProvider
851855 if ( ! currentTask || currentTask . abandoned || currentTask . abort ) {
852856 await this . removeClineFromStack ( )
853857 }
858+
859+ // Ensure zoo-gateway profile is seeded for users who signed in before this feature existed.
860+ // Without this, users with a valid cached token but no zoo-gateway profile would need to
861+ // re-authenticate to use Zoo Gateway. Fire-and-forget to avoid blocking webview init.
862+ void this . ensureZooGatewayProfileSeeded ( ) . catch ( ( err ) => {
863+ this . log ( `[ensureZooGatewayProfileSeeded] Error: ${ err instanceof Error ? err . message : String ( err ) } ` )
864+ } )
865+ }
866+
867+ /**
868+ * Seeds the zoo-gateway provider profile for users who have a cached auth token
869+ * but no profile (e.g., users who signed in before Zoo Gateway was added), or
870+ * who have an empty/imported profile without a token.
871+ * Called once per webview init; handleZooCodeCallback is idempotent so repeated calls are safe.
872+ */
873+ private async ensureZooGatewayProfileSeeded ( ) : Promise < void > {
874+ const { getCachedZooCodeToken } = await import ( "../../services/zoo-code-auth" )
875+ const token = getCachedZooCodeToken ( )
876+ if ( ! token ) return
877+
878+ // Check ALL zoo-gateway profiles — only skip seeding if every profile has the current token.
879+ // Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback
880+ // uses .filter() and updates all of them — the early-return guard must match.
881+ const allProfiles = await this . providerSettingsManager . listConfig ( )
882+ const zooGatewayProfiles = allProfiles . filter ( ( p ) => p . apiProvider === "zoo-gateway" )
883+
884+ if ( zooGatewayProfiles . length === 0 ) {
885+ this . log ( "[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one" )
886+ } else {
887+ let allUpToDate = true
888+
889+ for ( const entry of zooGatewayProfiles ) {
890+ try {
891+ const fullProfile = await this . providerSettingsManager . getProfile ( { name : entry . name } )
892+ if ( fullProfile . zooSessionToken !== token ) {
893+ allUpToDate = false
894+ this . log (
895+ fullProfile . zooSessionToken
896+ ? "[ensureZooGatewayProfileSeeded] Token mismatch (stale session?), updating with current token"
897+ : "[ensureZooGatewayProfileSeeded] Existing zoo-gateway profile has no token, updating with cached token" ,
898+ )
899+ break
900+ }
901+ } catch {
902+ allUpToDate = false
903+ this . log ( "[ensureZooGatewayProfileSeeded] Failed to read existing profile, will re-seed" )
904+ break
905+ }
906+ }
907+
908+ if ( allUpToDate ) {
909+ // All profiles have the current token — nothing to do
910+ return
911+ }
912+ }
913+
914+ // User has token but either no profile, some profiles without token, or stale tokens — seed all
915+ await this . handleZooCodeCallback ( token )
854916 }
855917
856918 public async createTaskWithHistoryItem (
@@ -1641,12 +1703,80 @@ export class ClineProvider
16411703 await this . upsertProviderProfile ( currentApiConfigName , newConfiguration )
16421704 }
16431705
1644- // Zoo Code Auth (for observability telemetry)
1706+ // Zoo Code Auth
16451707
1646- async handleZooCodeCallback ( _token : string ) {
1708+ async handleZooCodeCallback ( token : string ) {
16471709 // Auth mutation (token storage, subscription check, success toast) was already
16481710 // performed by handleAuthCallback() in handleUri.ts before this method was called.
1649- // This method only needs to refresh the webview state to reflect the new auth status.
1711+ // Save the zoo-gateway provider profile with the session token so that
1712+ // ZooGatewayHandler can authenticate without any manual user input.
1713+ //
1714+ // activate: true ONLY if Zoo Gateway is already the active profile — this pushes
1715+ // the new token to the in-memory handler so the current task picks it up immediately.
1716+ // Otherwise activate: false — do NOT switch providers mid-conversation. The user
1717+ // must explicitly select Zoo Gateway in settings if they want to use it.
1718+ try {
1719+ const { apiConfiguration } = await this . getState ( )
1720+ const currentSettings = this . contextProxy . getProviderSettings ( )
1721+ const currentApiConfigName = this . contextProxy . getValues ( ) . currentApiConfigName
1722+
1723+ // Derive the gateway base URL from ZOO_CODE_BASE_URL so that non-prod environments
1724+ // (staging, local dev) route completions to the correct backend instead of always
1725+ // hard-coding production. An already-set value in the profile is NOT preserved here —
1726+ // it must always align with the auth server the user just authenticated against.
1727+ const { getZooCodeBaseUrl } = await import ( "../../services/zoo-code-auth" )
1728+ const derivedGatewayBaseUrl = `${ getZooCodeBaseUrl ( ) } /api/gateway/v1`
1729+
1730+ // Check if Zoo Gateway is the currently active profile by apiProvider identity,
1731+ // not by profile name (profile names are user-renameable).
1732+ const isZooGatewayActive = currentSettings . apiProvider === "zoo-gateway"
1733+
1734+ // Always scan ALL profiles and update every zoo-gateway profile with the new token.
1735+ // This ensures renamed profiles, duplicate profiles, and inactive profiles all stay
1736+ // in sync. The model lookup in requestRouterModels uses .find() which returns the
1737+ // first zoo-gateway profile it finds — if that profile has a stale token, requests fail.
1738+ const allProfiles = await this . providerSettingsManager . listConfig ( )
1739+ const zooProfiles = allProfiles . filter ( ( p ) => p . apiProvider === "zoo-gateway" )
1740+
1741+ if ( zooProfiles . length === 0 ) {
1742+ // No existing zoo-gateway profile — create the canonical default.
1743+ const newConfiguration : ProviderSettings = {
1744+ apiProvider : "zoo-gateway" ,
1745+ zooSessionToken : token ,
1746+ zooGatewayModelId : apiConfiguration . zooGatewayModelId ,
1747+ zooGatewayBaseUrl : derivedGatewayBaseUrl ,
1748+ }
1749+ // Activate only if zoo-gateway was the active provider (shouldn't happen if
1750+ // no profiles exist, but defensive).
1751+ await this . upsertProviderProfile ( "Zoo Gateway" , newConfiguration , isZooGatewayActive )
1752+ } else {
1753+ // Update every existing zoo-gateway profile with the new token and the
1754+ // derived base URL so that environment-specific routing stays consistent.
1755+ for ( const entry of zooProfiles ) {
1756+ const isActiveProfile = isZooGatewayActive && entry . name === currentApiConfigName
1757+ const existing = await this . providerSettingsManager . getProfile ( { name : entry . name } )
1758+ const updated : ProviderSettings = {
1759+ ...existing ,
1760+ zooSessionToken : token ,
1761+ zooGatewayBaseUrl : derivedGatewayBaseUrl ,
1762+ }
1763+ if ( isActiveProfile ) {
1764+ // Use upsertProviderProfile with activate: true so the in-memory handler
1765+ // picks up the new token immediately for the current task.
1766+ await this . upsertProviderProfile ( entry . name , updated , true )
1767+ } else {
1768+ // Non-active profiles just need the token saved to disk.
1769+ await this . providerSettingsManager . saveConfig ( entry . name , updated )
1770+ }
1771+ }
1772+ }
1773+ } catch ( error ) {
1774+ this . log (
1775+ `[handleZooCodeCallback] Failed to save zoo-gateway profile: ${
1776+ error instanceof Error ? error . message : String ( error )
1777+ } `,
1778+ )
1779+ }
16501780 await this . postStateToWebview ( )
16511781 }
16521782
0 commit comments