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