privateboard 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1193,6 +1193,66 @@
1193
1193
  cursor: progress;
1194
1194
  }
1195
1195
  .na-avatar-regen-mark { font-size: 11px; color: var(--lime); line-height: 1; }
1196
+ /* Model picker chip · sits in the avatar column right under the
1197
+ regen button, so the user sees the active model the moment they
1198
+ open the modal (the foot-bottom placement was almost invisible).
1199
+ Sized to match .na-avatar-regen so the two reads as a paired
1200
+ control set: regen the face, switch the model. Same .cmp-dd
1201
+ markup as the AI composer's chip — these overrides only adjust
1202
+ the surface fit (padding / type scale / alignment). */
1203
+ .new-agent-overlay .na-avatar-model {
1204
+ display: inline-flex;
1205
+ align-items: center;
1206
+ gap: 8px;
1207
+ padding: 7px 12px;
1208
+ font-size: 10px;
1209
+ letter-spacing: 0.14em;
1210
+ text-transform: uppercase;
1211
+ border: 0.5px solid var(--line-bright);
1212
+ background: transparent;
1213
+ color: var(--text-soft);
1214
+ cursor: pointer;
1215
+ transition: border-color 0.12s, color 0.12s, background 0.12s;
1216
+ max-width: 220px;
1217
+ }
1218
+ .new-agent-overlay .na-avatar-model:hover {
1219
+ border-color: var(--lime);
1220
+ color: var(--lime);
1221
+ }
1222
+ .new-agent-overlay .na-avatar-model.open {
1223
+ border-color: var(--lime);
1224
+ color: var(--lime);
1225
+ background: var(--bg);
1226
+ }
1227
+ .new-agent-overlay .na-avatar-model .cmp-dd-label {
1228
+ color: var(--text-faint);
1229
+ font-weight: 500;
1230
+ letter-spacing: 0.14em;
1231
+ }
1232
+ .new-agent-overlay .na-avatar-model .cmp-dd-value {
1233
+ color: var(--text);
1234
+ font-weight: 700;
1235
+ letter-spacing: 0.04em;
1236
+ text-transform: none;
1237
+ font-size: 11px;
1238
+ /* Long model names (e.g. "Claude Opus 4.7 · 1M context") truncate
1239
+ instead of wrapping the chip into a second line. */
1240
+ white-space: nowrap;
1241
+ overflow: hidden;
1242
+ text-overflow: ellipsis;
1243
+ max-width: 140px;
1244
+ }
1245
+ .new-agent-overlay .na-avatar-model.open .cmp-dd-value { color: var(--lime); }
1246
+ .new-agent-overlay .na-avatar-model .cmp-dd-chevron {
1247
+ color: var(--text-faint);
1248
+ font-size: 9px;
1249
+ line-height: 1;
1250
+ transition: transform 0.12s;
1251
+ }
1252
+ .new-agent-overlay .na-avatar-model.open .cmp-dd-chevron {
1253
+ color: var(--lime);
1254
+ transform: rotate(180deg);
1255
+ }
1196
1256
  /* Vibe caption · the LLM's one-line description that seeded the avatar. */
1197
1257
  .na-avatar-vibe {
1198
1258
  font-family: var(--mono);
@@ -27,6 +27,10 @@
27
27
  { provider: "xai", models: [
28
28
  { v: "grok-4-3", name: "Grok 4.3", deck: "flagship · 1M ctx" },
29
29
  { v: "grok-4-1-fast", name: "Grok 4.1 Fast", deck: "fast · 256k ctx" }
30
+ ]},
31
+ { provider: "deepseek", models: [
32
+ { v: "deepseek-v4-pro", name: "DeepSeek V4 Pro", deck: "reasoning · open weights" },
33
+ { v: "deepseek-v4-flash", name: "DeepSeek Lite", deck: "V4 Flash · fast · 1M ctx" }
30
34
  ]}
31
35
  ];
32
36
  const ALL_MODELS = MODEL_GROUPS.flatMap((g) => g.models);
@@ -55,6 +59,34 @@
55
59
  }[c]));
56
60
  }
57
61
 
62
+ function t(k, vars) {
63
+ return (window.I18n && window.I18n.t(k, vars)) || k;
64
+ }
65
+
66
+ function applyNewAgentI18n() {
67
+ if (!modal) return;
68
+ modal.querySelectorAll("[data-i18n-na-html]").forEach((el) => {
69
+ const key = el.getAttribute("data-i18n-na-html");
70
+ if (key) el.innerHTML = t(key);
71
+ });
72
+ modal.querySelectorAll("[data-i18n-na]").forEach((el) => {
73
+ const key = el.getAttribute("data-i18n-na");
74
+ if (key && !el.hasAttribute("data-i18n-na-html")) el.textContent = t(key);
75
+ });
76
+ modal.querySelectorAll("[data-i18n-na-placeholder]").forEach((el) => {
77
+ const key = el.getAttribute("data-i18n-na-placeholder");
78
+ if (key) el.setAttribute("placeholder", t(key));
79
+ });
80
+ modal.querySelectorAll("[data-i18n-na-title]").forEach((el) => {
81
+ const key = el.getAttribute("data-i18n-na-title");
82
+ if (key) el.setAttribute("title", t(key));
83
+ });
84
+ modal.querySelectorAll("[data-i18n-na-aria]").forEach((el) => {
85
+ const key = el.getAttribute("data-i18n-na-aria");
86
+ if (key) el.setAttribute("aria-label", t(key));
87
+ });
88
+ }
89
+
58
90
  /* ───── 8-bit avatar generator ─────
59
91
  Pure function: same seed → same avatar.
60
92
  placeholder=true returns a neutral grey silhouette.
@@ -122,16 +154,16 @@
122
154
  <div class="new-agent-overlay" id="new-agent-overlay" role="dialog" aria-modal="true" aria-hidden="true">
123
155
  <div class="new-agent-modal" role="document">
124
156
  <div class="na-classification">
125
- <span><span class="dot">●</span> directors · new</span>
126
- <span class="right">// shape the role</span>
157
+ <span><span class="dot">●</span> <span data-i18n-na="na_class_left"></span></span>
158
+ <span class="right" data-i18n-na="na_class_right"></span>
127
159
  </div>
128
160
 
129
161
  <header class="na-head">
130
162
  <div>
131
- <div class="na-step-num">// new <span class="hl">director</span> · manual setup</div>
132
- <div class="na-step-title">shape the role</div>
163
+ <div class="na-step-num" data-i18n-na-html="na_step_kicker_html"></div>
164
+ <div class="na-step-title" data-i18n-na="na_step_title"></div>
133
165
  </div>
134
- <button type="button" class="na-close" aria-label="Close">✕</button>
166
+ <button type="button" class="na-close" data-i18n-na-aria="na_close_aria" aria-label="">✕</button>
135
167
  </header>
136
168
 
137
169
  <div class="na-body">
@@ -143,7 +175,23 @@
143
175
  </div>
144
176
  <button type="button" class="na-avatar-regen" data-na-regen>
145
177
  <span class="na-avatar-regen-mark">◆</span>
146
- <span class="na-avatar-regen-label">generate 8-bit avatar</span>
178
+ <span class="na-avatar-regen-label" data-i18n-na="na_avatar_regen"></span>
179
+ </button>
180
+ <!-- Model picker · reuses the cmp-dd chip + dropdown
181
+ machinery from the AI agent composer's model
182
+ dropdown. Reads + writes app.loadAgentComposerModel
183
+ so the user's last-picked model is shared across
184
+ both flows. Sized up via .na-avatar-model in
185
+ new-agent.css to match the regen button's visual
186
+ weight (the toolbar-sized chip is too small to
187
+ notice in a full-screen modal). The popover is
188
+ opened by app.openComposerDropdown via the
189
+ document-level [data-cmp-dropdown] click delegate
190
+ in app.js. -->
191
+ <button type="button" class="cmp-dd na-avatar-model" data-cmp-dropdown="agent-model" data-i18n-na-title="na_field_model">
192
+ <span class="cmp-dd-label" data-i18n-na="na_field_model"></span>
193
+ <span class="cmp-dd-value" data-cmp-dd-value="agent-model"></span>
194
+ <span class="cmp-dd-chevron">▾</span>
147
195
  </button>
148
196
  <div class="na-avatar-vibe" data-na-vibe></div>
149
197
  </div>
@@ -152,42 +200,35 @@
152
200
 
153
201
  <div class="na-field">
154
202
  <label class="na-field-label">
155
- <span>Name</span>
203
+ <span data-i18n-na="na_field_name"></span>
156
204
  <span class="na-field-meta"><span class="na-name-count">0</span>/32</span>
157
205
  </label>
158
206
  <div class="na-input-wrap na-name-wrap">
159
- <input type="text" class="na-name-input" placeholder="Aurelia · The Long-Cycle Strategist" maxlength="32">
207
+ <input type="text" class="na-name-input" data-i18n-na-placeholder="na_name_ph" placeholder="" maxlength="32">
160
208
  </div>
161
- <div class="na-field-hint">handle: <span class="na-handle-preview">/new_agent</span></div>
209
+ <div class="na-field-hint"><span data-i18n-na="na_hint_handle"></span> <span class="na-handle-preview">/new_agent</span></div>
162
210
  </div>
163
211
 
164
212
  <div class="na-field">
165
213
  <label class="na-field-label">
166
- <span>Intro</span>
214
+ <span data-i18n-na="na_field_intro"></span>
167
215
  <span class="na-field-meta"><span class="na-desc-count">0</span>/280</span>
168
216
  </label>
169
217
  <div class="na-textarea-wrap intro">
170
- <textarea class="na-desc-input" placeholder="One or two sentences · how this director shows up in a room. Reads everything on a hundred-year scale. Knows which patterns repeat and which never do." maxlength="280"></textarea>
218
+ <textarea class="na-desc-input" data-i18n-na-placeholder="na_intro_ph" placeholder="" maxlength="280"></textarea>
171
219
  </div>
172
- <div class="na-field-hint">becomes their public bio</div>
220
+ <div class="na-field-hint" data-i18n-na="na_hint_bio"></div>
173
221
  </div>
174
222
 
175
223
  <div class="na-field">
176
224
  <label class="na-field-label">
177
- <span>Instruction</span>
178
- <span class="na-field-meta"><span class="na-instr-count">0</span> chars · markdown</span>
225
+ <span data-i18n-na="na_field_instruction"></span>
226
+ <span class="na-field-meta"><span class="na-instr-count">0</span> · <span data-i18n-na="na_instr_meta"></span></span>
179
227
  </label>
180
228
  <div class="na-textarea-wrap tall">
181
- <textarea class="na-instr-input" spellcheck="false" placeholder="### Role
182
- You are __, the room's __. Your job is to ___.
183
-
184
- ### Voice
185
- Demand ___. Don't ___. Cite ___ when ___.
186
-
187
- ### Boundaries
188
- When the room ___, raise an objection."></textarea>
229
+ <textarea class="na-instr-input" spellcheck="false" data-i18n-na-placeholder="na_instr_ph" placeholder=""></textarea>
189
230
  </div>
190
- <div class="na-field-hint">applies to every room they join · skills, rules and model are configured later in the profile</div>
231
+ <div class="na-field-hint" data-i18n-na="na_hint_instr"></div>
191
232
  </div>
192
233
 
193
234
  </div>
@@ -195,12 +236,12 @@ When the room ___, raise an objection."></textarea>
195
236
  </div>
196
237
 
197
238
  <footer class="na-foot">
198
- <div class="na-foot-meta">configure skills · rules · model after creation</div>
239
+ <div class="na-foot-meta" data-i18n-na="na_foot_meta"></div>
199
240
  <div class="na-foot-actions">
200
- <button type="button" class="na-cancel">cancel</button>
241
+ <button type="button" class="na-cancel" data-i18n-na="na_cancel"></button>
201
242
  <button type="button" class="na-create" disabled>
202
243
  <span class="na-create-mark">◆</span>
203
- <span>create director</span>
244
+ <span data-i18n-na="na_create"></span>
204
245
  </button>
205
246
  </div>
206
247
  </footer>
@@ -229,13 +270,13 @@ When the room ___, raise an objection."></textarea>
229
270
  const addBtn = modal && modal.querySelector("[data-na-rule-add]");
230
271
  if (!list) return;
231
272
  if (rulesState.length === 0) {
232
- list.innerHTML = `<li class="na-rule-empty">no rules yet · directors will follow only their instruction</li>`;
273
+ list.innerHTML = `<li class="na-rule-empty">${escape(t("na_rule_empty"))}</li>`;
233
274
  } else {
234
275
  list.innerHTML = rulesState.map((body, i) => `
235
276
  <li class="na-rule" data-rule-idx="${i}">
236
277
  <span class="na-rule-num">${i + 1}</span>
237
- <input type="text" class="na-rule-input" placeholder="never preface · cite the load-bearing claim with **bold** · ..." maxlength="120" value="${escape(body)}">
238
- <button type="button" class="na-rule-rm" data-na-rule-rm="${i}" title="Remove">✕</button>
278
+ <input type="text" class="na-rule-input" placeholder="${escape(t("na_rule_ph"))}" maxlength="120" value="${escape(body)}">
279
+ <button type="button" class="na-rule-rm" data-na-rule-rm="${i}" title="${escape(t("na_rule_rm"))}">✕</button>
239
280
  </li>
240
281
  `).join("");
241
282
  }
@@ -282,9 +323,9 @@ When the room ___, raise an objection."></textarea>
282
323
  `);
283
324
  } else {
284
325
  slots.push(`
285
- <button type="button" class="na-skill-slot empty" data-na-skill-slot="${i}" title="Install ability">
326
+ <button type="button" class="na-skill-slot empty" data-na-skill-slot="${i}" title="${escape(t("na_skill_install"))}">
286
327
  <span class="na-skill-icon">+</span>
287
- <span class="na-skill-name">empty</span>
328
+ <span class="na-skill-name">${escape(t("na_skill_empty"))}</span>
288
329
  </button>
289
330
  `);
290
331
  }
@@ -363,9 +404,9 @@ When the room ___, raise an objection."></textarea>
363
404
 
364
405
  function getProviderStatus(provider) {
365
406
  const keys = (typeof window.boardroomKeys === "function" ? window.boardroomKeys() : {}) || {};
366
- if (keys[provider]) return { label: "direct", cls: "direct" };
367
- if (keys.openrouter) return { label: "via openrouter", cls: "via" };
368
- return { label: "no key", cls: "none" };
407
+ if (keys[provider]) return { label: t("na_key_direct"), cls: "direct" };
408
+ if (keys.openrouter) return { label: t("na_key_via"), cls: "via" };
409
+ return { label: t("na_key_none"), cls: "none" };
369
410
  }
370
411
 
371
412
  function refreshProviderStatus() {
@@ -391,10 +432,37 @@ When the room ___, raise an objection."></textarea>
391
432
  paintAvatar();
392
433
  refreshAll();
393
434
 
435
+ // Paint the model dropdown's current label · the chip uses the
436
+ // same agent-model state as the AI composer (loadAgentComposerModel),
437
+ // so re-opening the overlay reflects the user's last pick.
438
+ paintModelLabel();
439
+
394
440
  overlay.classList.add("open");
395
441
  overlay.setAttribute("aria-hidden", "false");
396
442
  document.body.style.overflow = "hidden";
397
443
  setTimeout(() => modal.querySelector(".na-name-input").focus(), 80);
444
+ applyNewAgentI18n();
445
+ refreshProviderStatus();
446
+ }
447
+
448
+ /** Read the user's current agent-model selection (shared with the
449
+ * AI composer) and paint it into the overlay's `cmp-dd-value` span.
450
+ * Falls back to a dash when app or its model resolver isn't ready. */
451
+ function paintModelLabel() {
452
+ const span = modal && modal.querySelector('[data-cmp-dd-value="agent-model"]');
453
+ if (!span) return;
454
+ let label = "—";
455
+ try {
456
+ if (window.app && typeof window.app.loadAgentComposerModel === "function") {
457
+ const v = window.app.loadAgentComposerModel();
458
+ if (v) {
459
+ label = (typeof window.app.modelLabel === "function")
460
+ ? (window.app.modelLabel(v) || v)
461
+ : v;
462
+ }
463
+ }
464
+ } catch (_) { /* keep dash */ }
465
+ span.textContent = label;
398
466
  }
399
467
 
400
468
  function close() {
@@ -511,8 +579,8 @@ When the room ___, raise an objection."></textarea>
511
579
  }
512
580
 
513
581
  if (btn) btn.disabled = true;
514
- const originalLabel = labelEl?.textContent || "generate 8-bit avatar";
515
- if (labelEl) labelEl.textContent = "thinking…";
582
+ const originalLabel = labelEl?.textContent || t("na_avatar_regen");
583
+ if (labelEl) labelEl.textContent = t("na_avatar_thinking");
516
584
 
517
585
  try {
518
586
  const res = await fetch("/api/avatar/generate", {
@@ -595,6 +663,9 @@ When the room ___, raise an objection."></textarea>
595
663
  // Initial paint of placeholder
596
664
  paintAvatar();
597
665
 
666
+ document.addEventListener("boardroom:locale", applyNewAgentI18n);
667
+ applyNewAgentI18n();
668
+
598
669
  // Create — POST to /api/agents and refresh the sidebar's agents list.
599
670
  modal.querySelector(".na-create").addEventListener("click", async () => {
600
671
  const create = modal.querySelector(".na-create");
@@ -603,20 +674,16 @@ When the room ___, raise an objection."></textarea>
603
674
  const name = modal.querySelector(".na-name-input").value.trim();
604
675
  const bio = modal.querySelector(".na-desc-input").value.trim();
605
676
  const instruction = modal.querySelector(".na-instr-input").value.trim();
606
- // Default model · resolved from the user's current key set via
607
- // the shared /api/models cache (`defaultModelV` field). Without
608
- // this, new agents were always born with `opus-4-7` even when
609
- // the user only had a direct OpenAI key — the agent would then
610
- // hit `NoKeyError` on every turn until the user manually
611
- // changed its model. The cache may not have loaded yet (very
612
- // first interaction); we fall back to `opus-4-7` only as a
613
- // last resort and leave the runtime resolver to fix it up.
677
+ // Model · the foot dropdown writes the user's pick to the shared
678
+ // agent-composer model state (loadAgentComposerModel). It
679
+ // already does the reachability + default-resolver fallback
680
+ // chain (server defaultModelV first reachable DEFAULT_AGENT_MODEL),
681
+ // so this just reads it back. Hard fallback to "opus-4-7" only
682
+ // covers the edge case where window.app isn't initialized.
614
683
  let modelV = "opus-4-7";
615
- const cache = (typeof window.boardroomModels === "function") ? window.boardroomModels() : null;
616
- if (cache && typeof cache.defaultModelV === "string" && cache.defaultModelV) {
617
- modelV = cache.defaultModelV;
618
- } else if (cache && Array.isArray(cache.reachable) && cache.reachable.length > 0) {
619
- modelV = cache.reachable[0].modelV;
684
+ if (window.app && typeof window.app.loadAgentComposerModel === "function") {
685
+ const picked = window.app.loadAgentComposerModel();
686
+ if (picked) modelV = picked;
620
687
  }
621
688
 
622
689
  // Avatar → data URL. If the user never clicked "regenerate", we
@@ -631,9 +698,8 @@ When the room ___, raise an objection."></textarea>
631
698
  const avatarPath = "data:image/svg+xml;utf8," + encodeURIComponent(svg);
632
699
 
633
700
  // Lock the button while the request is in flight.
634
- const orig = create.textContent;
635
701
  create.disabled = true;
636
- create.textContent = "[ creating… ]";
702
+ create.innerHTML = `<span class="na-create-mark">◆</span><span>${escape(t("na_creating"))}</span>`;
637
703
 
638
704
  try {
639
705
  const res = await fetch("/api/agents", {
@@ -658,9 +724,11 @@ When the room ___, raise an objection."></textarea>
658
724
  } catch (_) { /* */ }
659
725
  close();
660
726
  } catch (e) {
661
- alert("Couldn't create the director: " + (e && e.message ? e.message : e));
727
+ const msg = e && e.message ? e.message : String(e);
728
+ alert(t("na_create_fail", { msg }));
662
729
  create.disabled = false;
663
- create.textContent = orig;
730
+ create.innerHTML = `<span class="na-create-mark">◆</span><span data-i18n-na="na_create"></span>`;
731
+ applyNewAgentI18n();
664
732
  }
665
733
  });
666
734
  }
@@ -124,6 +124,7 @@
124
124
  * new model the catalog doesn't know about yet). */
125
125
  const MODEL_LABELS = {
126
126
  "sonnet-4-6": "Sonnet 4.6",
127
+ "opus-4-6": "Opus 4.6",
127
128
  "opus-4-7": "Opus 4.7",
128
129
  "opus-4-6": "Opus 4.6",
129
130
  "opus-4-6-fast": "Opus 4.6 Fast",
@@ -138,9 +139,9 @@
138
139
  "gemini-3-1-flash": "Gemini 3.1 Flash Lite",
139
140
  "grok-4-3": "Grok 4.3",
140
141
  "grok-4-1-fast": "Grok 4.1 Fast",
141
- "grok-4-3": "Grok 4.3",
142
142
  "grok-4-20": "Grok 4.20",
143
143
  "deepseek-v4-pro": "DeepSeek V4 Pro",
144
+ "deepseek-v4-flash": "DeepSeek Lite",
144
145
  };
145
146
  function modelLabelFor(v) {
146
147
  if (!v) return "";
@@ -343,6 +343,43 @@
343
343
  min-width: 0;
344
344
  }
345
345
 
346
+ /* Preferences → Other · EN / Zh (moved off the sidebar chrome) */
347
+ .us-pane-body .locale-switch {
348
+ display: inline-flex;
349
+ align-items: center;
350
+ padding: 1px;
351
+ border: 1px solid var(--line-bright, rgba(255, 255, 255, 0.09));
352
+ border-radius: 3px;
353
+ background: var(--panel-2, #151514);
354
+ }
355
+ .us-pane-body .locale-switch .locale-btn {
356
+ appearance: none;
357
+ border: 0;
358
+ background: transparent;
359
+ color: var(--text-dim);
360
+ font-family: var(--mono);
361
+ font-size: 10px;
362
+ font-weight: 600;
363
+ padding: 3px 8px;
364
+ cursor: pointer;
365
+ border-radius: 2px;
366
+ line-height: 1.2;
367
+ }
368
+ .us-pane-body .locale-switch .locale-btn:hover { color: var(--text-soft); }
369
+ .us-pane-body .locale-switch .locale-btn.active {
370
+ background: var(--panel-3);
371
+ color: var(--lime, #6fb572);
372
+ }
373
+ .us-locale-deck {
374
+ margin: 10px 0 0;
375
+ padding: 0;
376
+ max-width: 420px;
377
+ font-size: 11px;
378
+ color: var(--text-faint, #3a382f);
379
+ letter-spacing: 0.02em;
380
+ line-height: 1.5;
381
+ }
382
+
346
383
  /* User avatar block */
347
384
  .us-avatar-row {
348
385
  display: flex;
@@ -639,6 +676,20 @@
639
676
  border-color: var(--red, #B5706A);
640
677
  color: var(--red, #B5706A);
641
678
  }
679
+ /* Locked variant · the only configured LLM key cannot be removed —
680
+ the boardroom would land in a "no usable carrier" state and every
681
+ director / chair / brief writer would fail. Renders dimmed and
682
+ non-interactive; the title attribute carries the explanation
683
+ ("Add another LLM key first — at least one must remain
684
+ configured."). The user-facing path to "delete this key" is to
685
+ add another LLM provider first, then come back to this row. */
686
+ .us-key-remove.is-locked,
687
+ .us-key-remove.is-locked:hover {
688
+ cursor: not-allowed;
689
+ opacity: 0.45;
690
+ border-color: var(--line-bright, #2A2A26);
691
+ color: var(--text-faint, #3A382F);
692
+ }
642
693
 
643
694
  /* "Set as default" CTA · sits next to the remove button on configured
644
695
  non-default LLM rows. Quiet outline by default, flips to lime on
@@ -1352,6 +1403,23 @@
1352
1403
  margin: -4px 0 6px;
1353
1404
  }
1354
1405
 
1406
+ .us-ws-backend-radios {
1407
+ display: flex;
1408
+ flex-wrap: wrap;
1409
+ gap: 14px 22px;
1410
+ }
1411
+ .us-ws-backend-label {
1412
+ display: inline-flex;
1413
+ align-items: center;
1414
+ gap: 8px;
1415
+ font-size: 12px;
1416
+ color: var(--text-soft, #8E8B83);
1417
+ cursor: pointer;
1418
+ }
1419
+ .us-ws-backend-label span {
1420
+ color: var(--text, #C8C5BE);
1421
+ }
1422
+
1355
1423
  /* ─── Available models summary + default picker ───
1356
1424
  Lives at the bottom of the API Key section. Pure-list layout —
1357
1425
  no shadows, no left accents. Provider tag + flat per-model rows