Inline OAuth pending controls under provider cards

Move device-code, manual callback, and setup controls into the provider row that started the OAuth flow so pending state stays anchored to its provider. Add static coverage and plugin DOX guidance for inline provider detail rendering.

Alessandro committed Jun 4, 2026 at 11:40 UTC 3facd68797aa7f4cb266c7c9c2c50d9a60318018
4 files changed +126 -122
plugins/_oauth/AGENTS.md
+1
@@ -25,6 +25,7 @@
25 - Add OAuth model providers in `_oauth/conf/model_providers.yaml`, not `_model_config/provider_metadata.yaml`.
26 - Provider cards and model slot actions must be driven by backend provider status. Do not reintroduce hardcoded frontend provider lists or fallback provider catalogs.
27 - OAuth account surfaces in settings, discovery, and onboarding must use the provider registry/status summary rather than Codex-only frontend state.
28 +- OAuth settings pending-auth controls such as device codes, manual callback input, and provider setup fields must render inline under the relevant provider row, not as a detached section below all providers.
29 - OAuth settings model slots must keep provider choice editable per slot, list only connected OAuth account providers, and persist the selected provider IDs into `chat_model.provider` and `utility_model.provider`.
30 - `helpers/providers/registry.py` is the source of truth for connectable OAuth providers.
31 - OAuth provider config must not expose the dummy `oauth` API key in `conf/model_providers.yaml`; the dummy key is a runtime-only shim supplied by the `get_api_key` extension after the account provider reports connected.
plugins/_oauth/webui/config.html
+88 -119
@@ -89,117 +89,91 @@
89 </div>
90 </template>
91 </div>
92 +
93 + <div class="oauth-provider-row-detail" x-show="$store.oauthConfig.providerDetailOpen(card.provider_id)" @click.stop>
94 + <div class="oauth-detail-grid">
95 + <label class="oauth-provider-input" x-show="$store.oauthConfig.providerShowSetupFields(card.provider_id) && card.supports_enterprise_domain">
96 + <span>Enterprise domain</span>
97 + <input
98 + type="text"
99 + x-model="$store.oauthConfig.providerUiFor(card.provider_id).enterprise_domain"
100 + name="enterprise_domain"
101 + placeholder="github.com"
102 + />
103 + </label>
104 +
105 + <div class="oauth-provider-fields" x-show="$store.oauthConfig.providerShowSetupFields(card.provider_id) && card.supports_oauth_client_config">
106 + <label class="oauth-provider-input">
107 + <span>OAuth client ID</span>
108 + <input
109 + type="text"
110 + x-model="$store.oauthConfig.providerUiFor(card.provider_id).client_id"
111 + name="oauth_client_id"
112 + placeholder="Google Cloud OAuth client ID"
113 + />
114 + </label>
115 + <label class="oauth-provider-input">
116 + <span>OAuth client secret</span>
117 + <input
118 + type="password"
119 + x-model="$store.oauthConfig.providerUiFor(card.provider_id).client_secret"
120 + name="oauth_client_secret"
121 + autocomplete="off"
122 + placeholder="Google Cloud OAuth client secret"
123 + />
124 + </label>
125 + <label class="oauth-provider-input" x-show="card.supports_quota_project">
126 + <span>Quota project</span>
127 + <input
128 + type="text"
129 + x-model="$store.oauthConfig.providerUiFor(card.provider_id).quota_project_id"
130 + name="quota_project_id"
131 + placeholder="Optional Google Cloud project ID"
132 + />
133 + </label>
134 + </div>
135 +
136 + <div class="oauth-device" x-show="$store.oauthConfig.providerDevice(card.provider_id)?.user_code">
137 + <span>Enter this code</span>
138 + <strong x-text="$store.oauthConfig.providerDevice(card.provider_id)?.user_code"></strong>
139 + <button class="text-button" type="button" @click="$store.oauthConfig.cancelConnect(card.provider_id)">
140 + <span class="material-symbols-outlined">close</span>
141 + <span>Cancel</span>
142 + </button>
143 + </div>
144 +
145 + <div class="oauth-manual-callback" x-show="card.supports_manual_callback && $store.oauthConfig.providerDevice(card.provider_id)?.flow === 'browser_pkce' && !card.connected">
146 + <input
147 + type="text"
148 + x-model="$store.oauthConfig.providerUiFor(card.provider_id).manualCallback"
149 + placeholder="Paste callback URL, query string, or code"
150 + />
151 + <button type="button" class="oauth-connect secondary" @click="$store.oauthConfig.submitManualCallback(card.provider_id)">
152 + <span class="material-symbols-outlined">check</span>
153 + <span>Submit</span>
154 + </button>
155 + <button type="button" class="oauth-connect secondary" @click="$store.oauthConfig.cancelConnect(card.provider_id)">
156 + <span class="material-symbols-outlined">close</span>
157 + <span>Cancel</span>
158 + </button>
159 + </div>
160 +
161 + <div class="oauth-auth-attempt" x-show="$store.oauthConfig.providerDevice(card.provider_id) && !$store.oauthConfig.providerDevice(card.provider_id)?.user_code && !card.supports_manual_callback">
162 + <span>Sign-in started</span>
163 + <button class="text-button" type="button" @click="$store.oauthConfig.cancelConnect(card.provider_id)">
164 + <span class="material-symbols-outlined">close</span>
165 + <span>Cancel</span>
166 + </button>
167 + </div>
168 +
169 + <p class="oauth-provider-note" x-show="$store.oauthConfig.providerShowNote(card.provider_id)" x-text="card.warning || card.note"></p>
170 + </div>
171 + </div>
172 </article>
173 </template>
174 </div>
175 </section>
176
97 - <template x-if="$store.oauthConfig.selectedProvider() && !$store.oauthConfig.selectedProvider().connected">
98 - <section class="oauth-provider-detail">
99 - <div class="oauth-provider-detail-head">
100 - <div class="oauth-provider-head">
101 - <div class="oauth-mark" :class="{ connected: $store.oauthConfig.selectedProvider().connected }">
102 - <span class="material-symbols-outlined" x-text="$store.oauthConfig.selectedProvider().connected ? 'check' : $store.oauthConfig.selectedProvider().mark"></span>
103 - </div>
104 - <div class="oauth-copy">
105 - <h2 x-text="$store.oauthConfig.selectedProvider().display_name"></h2>
106 - <p x-text="$store.oauthConfig.providerStatusLabel($store.oauthConfig.selectedProviderId)"></p>
107 - </div>
108 - </div>
109 - <div class="oauth-primary">
110 - <button
111 - class="oauth-connect"
112 - type="button"
113 - @click="$store.oauthConfig.connectProvider($store.oauthConfig.selectedProviderId)"
114 - :disabled="$store.oauthConfig.providerPrimaryDisabled($store.oauthConfig.selectedProviderId) || !$store.oauthConfig.providerSetupReady($store.oauthConfig.selectedProviderId)"
115 - x-show="!$store.oauthConfig.selectedProvider().connected"
116 - >
117 - <span class="material-symbols-outlined" x-text="$store.oauthConfig.connectingProvider === $store.oauthConfig.selectedProviderId ? 'progress_activity' : 'login'"></span>
118 - <span x-text="$store.oauthConfig.providerPrimaryLabel($store.oauthConfig.selectedProviderId)"></span>
119 - </button>
120 - </div>
121 - </div>
122 -
123 - <div class="oauth-detail-grid">
124 - <label class="oauth-provider-input" x-show="$store.oauthConfig.selectedProvider().supports_enterprise_domain && !$store.oauthConfig.selectedProvider().connected">
125 - <span>Enterprise domain</span>
126 - <input
127 - type="text"
128 - x-model="$store.oauthConfig.providerUiFor($store.oauthConfig.selectedProviderId).enterprise_domain"
129 - name="enterprise_domain"
130 - placeholder="github.com"
131 - />
132 - </label>
133 -
134 - <div class="oauth-provider-fields" x-show="$store.oauthConfig.selectedProvider().supports_oauth_client_config && !$store.oauthConfig.selectedProvider().connected">
135 - <label class="oauth-provider-input">
136 - <span>OAuth client ID</span>
137 - <input
138 - type="text"
139 - x-model="$store.oauthConfig.providerUiFor($store.oauthConfig.selectedProviderId).client_id"
140 - name="oauth_client_id"
141 - placeholder="Google Cloud OAuth client ID"
142 - />
143 - </label>
144 - <label class="oauth-provider-input">
145 - <span>OAuth client secret</span>
146 - <input
147 - type="password"
148 - x-model="$store.oauthConfig.providerUiFor($store.oauthConfig.selectedProviderId).client_secret"
149 - name="oauth_client_secret"
150 - autocomplete="off"
151 - placeholder="Google Cloud OAuth client secret"
152 - />
153 - </label>
154 - <label class="oauth-provider-input" x-show="$store.oauthConfig.selectedProvider().supports_quota_project">
155 - <span>Quota project</span>
156 - <input
157 - type="text"
158 - x-model="$store.oauthConfig.providerUiFor($store.oauthConfig.selectedProviderId).quota_project_id"
159 - name="quota_project_id"
160 - placeholder="Optional Google Cloud project ID"
161 - />
162 - </label>
163 - </div>
164 -
165 - <div class="oauth-device" x-show="$store.oauthConfig.devices[$store.oauthConfig.selectedProviderId]?.user_code">
166 - <span>Enter this code</span>
167 - <strong x-text="$store.oauthConfig.devices[$store.oauthConfig.selectedProviderId]?.user_code"></strong>
168 - <button class="text-button" type="button" @click="$store.oauthConfig.cancelConnect($store.oauthConfig.selectedProviderId)">
169 - <span class="material-symbols-outlined">close</span>
170 - <span>Cancel</span>
171 - </button>
172 - </div>
173 -
174 - <div class="oauth-manual-callback" x-show="$store.oauthConfig.selectedProvider().supports_manual_callback && $store.oauthConfig.devices[$store.oauthConfig.selectedProviderId]?.flow === 'browser_pkce' && !$store.oauthConfig.selectedProvider().connected">
175 - <input
176 - type="text"
177 - x-model="$store.oauthConfig.providerUiFor($store.oauthConfig.selectedProviderId).manualCallback"
178 - placeholder="Paste callback URL, query string, or code"
179 - />
180 - <button type="button" class="oauth-connect secondary" @click="$store.oauthConfig.submitManualCallback($store.oauthConfig.selectedProviderId)">
181 - <span class="material-symbols-outlined">check</span>
182 - <span>Submit</span>
183 - </button>
184 - <button type="button" class="oauth-connect secondary" @click="$store.oauthConfig.cancelConnect($store.oauthConfig.selectedProviderId)">
185 - <span class="material-symbols-outlined">close</span>
186 - <span>Cancel</span>
187 - </button>
188 - </div>
189 -
190 - <div class="oauth-auth-attempt" x-show="$store.oauthConfig.devices[$store.oauthConfig.selectedProviderId] && !$store.oauthConfig.devices[$store.oauthConfig.selectedProviderId]?.user_code && !$store.oauthConfig.selectedProvider().supports_manual_callback">
191 - <span>Sign-in started</span>
192 - <button class="text-button" type="button" @click="$store.oauthConfig.cancelConnect($store.oauthConfig.selectedProviderId)">
193 - <span class="material-symbols-outlined">close</span>
194 - <span>Cancel</span>
195 - </button>
196 - </div>
197 -
198 - <p class="oauth-provider-note" x-show="$store.oauthConfig.selectedProvider().warning || $store.oauthConfig.selectedProvider().note" x-text="$store.oauthConfig.selectedProvider().warning || $store.oauthConfig.selectedProvider().note"></p>
199 - </div>
200 - </section>
201 - </template>
202 -
177 <section class="oauth-model-config">
178 <div class="oauth-section-head">
179 <div>
@@ -375,8 +349,7 @@
349 color: var(--color-text);
350 }
351
378 - .oauth-provider-list,
379 - .oauth-provider-detail {
352 + .oauth-provider-list {
353 display: grid;
354 gap: 12px;
355 padding: 0;
@@ -477,11 +450,11 @@
450 padding: 4px 0 0;
451 }
452
480 - .oauth-provider-detail-head {
481 - display: flex;
482 - align-items: center;
483 - justify-content: space-between;
484 - gap: 12px;
453 + .oauth-provider-row-detail {
454 + display: grid;
455 + grid-column: 1 / -1;
456 + min-width: 0;
457 + padding-top: 2px;
458 }
459
460 .oauth-detail-grid {
@@ -1154,6 +1127,7 @@
1127
1128 @media (max-width: 720px) {
1129 .oauth-device,
1130 + .oauth-provider-row-detail,
1131 .oauth-provider-usage,
1132 .oauth-status-row,
1133 .oauth-model-grid,
@@ -1162,15 +1136,10 @@
1136 .oauth-details div,
1137 .oauth-manual-callback,
1138 .oauth-auth-attempt,
1165 - .oauth-provider-row,
1166 - .oauth-provider-detail-head {
1139 + .oauth-provider-row {
1140 grid-template-columns: 1fr;
1141 }
1142
1170 - .oauth-provider-detail-head {
1171 - display: grid;
1172 - }
1173 -
1143 .oauth-section-head {
1144 flex-direction: column;
1145 }
plugins/_oauth/webui/oauth-config-store.js
+28
@@ -299,6 +299,34 @@ export const store = createStore("oauthConfig", {
299 return status.account_label || status.email || "Connected";
300 },
301
302 + providerDevice(providerId) {
303 + return this.devices[String(providerId || "")] || null;
304 + },
305 +
306 + providerShowSetupFields(providerId) {
307 + if (this.providerConnected(providerId)) return false;
308 + if (this.providerDevice(providerId)) return false;
309 + return this.selectedProviderId === providerId;
310 + },
311 +
312 + providerShowNote(providerId) {
313 + if (this.providerConnected(providerId)) return false;
314 + const status = this.providerStatus(providerId);
315 + return this.selectedProviderId === providerId && Boolean(status.warning || status.note);
316 + },
317 +
318 + providerDetailOpen(providerId) {
319 + if (!this.isOauthProvider(providerId) || this.providerConnected(providerId)) return false;
320 + if (this.providerDevice(providerId)) return true;
321 + const status = this.providerStatus(providerId);
322 + return this.selectedProviderId === providerId && Boolean(
323 + status.supports_enterprise_domain
324 + || status.supports_oauth_client_config
325 + || status.warning
326 + || status.note
327 + );
328 + },
329 +
330 providerReadinessLabel(providerId) {
331 const status = this.providerStatus(providerId);
332 if (this.loadingStatus) return "Checking";
tests/test_oauth_static.py
+9 -3
@@ -52,9 +52,14 @@ def test_oauth_settings_exposes_provider_specific_controls_and_generic_copy():
52 assert "supports_quota_project" in config_html + store_js
53 assert "OAuth client ID" in config_html
54 assert "quota_project_id" in config_html + store_js
55 - assert "submitManualCallback($store.oauthConfig.selectedProviderId)" in config_html
56 - assert "cancelConnect($store.oauthConfig.selectedProviderId)" in config_html
55 + assert "providerDetailOpen(card.provider_id)" in config_html + store_js
56 + assert "providerDevice(card.provider_id)?.user_code" in config_html
57 + assert "submitManualCallback(card.provider_id)" in config_html
58 + assert "cancelConnect(card.provider_id)" in config_html
59 + assert "selectedProvider()" not in config_html
60 assert "oauth-auth-attempt" in config_html
61 + assert "oauth-provider-row-detail" in config_html
62 + assert "oauth-provider-detail" not in config_html
63 assert "oauth-detail-metrics" not in config_html
64 assert "Codex/ChatGPT Account" not in config_html + store_js
65 assert "activeModelsDescription()" in config_html + store_js
@@ -88,7 +93,8 @@ def test_oauth_available_models_list_sits_above_advanced_without_borders():
93 assert config_html.index("<h3>Providers</h3>") < config_html.index("<summary>Advanced</summary>")
94 assert config_html.index("Available models") < config_html.index("<summary>Advanced</summary>")
95 assert ".oauth-models-panel {\n display: grid;\n gap: 10px;\n padding: 0;\n border: 0;\n }" in config_html
91 - assert ".oauth-provider-list,\n .oauth-provider-detail {\n display: grid;\n gap: 12px;\n padding: 0;\n border: 0;" in config_html
96 + assert ".oauth-provider-list {\n display: grid;\n gap: 12px;\n padding: 0;\n border: 0;" in config_html
97 + assert ".oauth-provider-row-detail {\n display: grid;\n grid-column: 1 / -1;" in config_html
98 assert ".oauth-advanced {\n border: 0;\n border-radius: 0;\n padding: 0;\n }" in config_html
99 model_chip_rule = config_html.split(".oauth-models span {", 1)[1].split("}", 1)[0]
100 assert "border:" not in model_chip_rule