@jxsuite/studio 4.0.0 → 4.1.0

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.
Files changed (86) hide show
  1. package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
  2. package/dist/chunks/{Overlay.dev-723bv0df.js → Overlay.dev-etqjwyf9.js} +4 -4
  3. package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
  4. package/dist/chunks/{content-source-t88vb2dr.js → content-source-48xg86wj.js} +17 -17
  5. package/dist/chunks/{convert-to-repeater-dhw1nrz9.js → convert-to-repeater-43tfn7cs.js} +24 -24
  6. package/dist/chunks/{entry-commands-6z6bhxg0.js → entry-commands-e63f1xc0.js} +20 -20
  7. package/dist/chunks/{extension-sections-3ydgjbdy.js → extension-sections-txv08xxg.js} +23 -23
  8. package/dist/chunks/{file-ops-rskmhxyq.js → file-ops-wbh4hhsx.js} +16 -16
  9. package/dist/chunks/{files-0cratsr0.js → files-3kfkwhk4.js} +25 -24
  10. package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
  11. package/dist/chunks/{format-host-tgcmfs4p.js → format-host-w16q81tf.js} +2 -2
  12. package/dist/chunks/fs-events-hrrg328g.js +1 -1
  13. package/dist/chunks/{git-panel-0yywyk76.js → git-panel-532whr1y.js} +16 -15
  14. package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
  15. package/dist/chunks/{jx-validate-8q2g05x1.js → jx-validate-8er6gbxy.js} +3 -3
  16. package/dist/chunks/{layers-panel-z3tqe5cd.js → layers-panel-07hm7g5j.js} +19 -19
  17. package/dist/chunks/{monaco-setup-rws7k4br.js → monaco-setup-vx9bhp1d.js} +2 -2
  18. package/dist/chunks/{publish-panel-54jvhskq.js → publish-panel-v9st96ja.js} +11 -10
  19. package/dist/chunks/{quick-search-cgfmjwq5.js → quick-search-zccjp6gv.js} +26 -25
  20. package/dist/chunks/{right-panel-cqp8bc4v.js → right-panel-vm3b6kwy.js} +17 -17
  21. package/dist/chunks/{sp-overlay.dev-yztmtrs1.js → sp-overlay.dev-qgvhnxf4.js} +5 -5
  22. package/dist/chunks/{sp-tooltip.dev-xcvpnqr0.js → sp-tooltip.dev-4sastdnw.js} +3 -3
  23. package/dist/chunks/{studio-txwkm6p9.js → studio-0bq6eh7v.js} +2 -2
  24. package/dist/chunks/studio-1ngbejft.js +181 -0
  25. package/dist/chunks/{studio-gv8vvshc.js → studio-1wagb550.js} +3 -3
  26. package/dist/chunks/{studio-kjjsjak4.js → studio-2b946z26.js} +58 -57
  27. package/dist/chunks/{studio-4pqqxbrt.js → studio-3bqpr0zh.js} +16 -16
  28. package/dist/chunks/{studio-v38hmvx1.js → studio-4b5z1x43.js} +42 -8
  29. package/dist/chunks/{studio-9c4k4vhv.js → studio-4n2b5gzb.js} +4 -4
  30. package/dist/chunks/{studio-p8j86wag.js → studio-4r1pv6zf.js} +2 -2
  31. package/dist/chunks/{studio-mmv72a27.js → studio-5fx3g9jg.js} +29 -48
  32. package/dist/chunks/{studio-5rywrkxz.js → studio-5myknm0a.js} +42 -42
  33. package/dist/chunks/{studio-sf7zjgv1.js → studio-6yw4y3my.js} +3 -3
  34. package/dist/chunks/{studio-pdfjntbk.js → studio-950k0dx6.js} +35 -35
  35. package/dist/chunks/{studio-eenfkt4e.js → studio-ahn3pdgx.js} +5 -5
  36. package/dist/chunks/{studio-eesa1myv.js → studio-b55y6ewx.js} +9 -9
  37. package/dist/chunks/{studio-cb12yz9w.js → studio-cmb03bg5.js} +99 -1264
  38. package/dist/chunks/studio-dm19q43r.js +195 -0
  39. package/dist/chunks/{studio-ahjfdy6e.js → studio-ef49pnzx.js} +6 -6
  40. package/dist/chunks/{studio-wx5609xd.js → studio-ew0djmhg.js} +10 -10
  41. package/dist/chunks/{studio-9r6j5ev1.js → studio-f3wqjtk5.js} +4 -3
  42. package/dist/chunks/{studio-8swrjbny.js → studio-fbf92ckf.js} +14 -14
  43. package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
  44. package/dist/chunks/{studio-ex8zyp98.js → studio-gfgb8pe6.js} +23 -184
  45. package/dist/chunks/{studio-r1r7afxx.js → studio-jc994dnr.js} +24 -24
  46. package/dist/chunks/{studio-yxrkyv8a.js → studio-jkftxyvz.js} +52 -3416
  47. package/dist/chunks/{studio-hbtp0eh1.js → studio-msd847zn.js} +13 -13
  48. package/dist/chunks/{studio-kyk2v92a.js → studio-mtfnaqs9.js} +14 -14
  49. package/dist/chunks/{studio-vadzgkrz.js → studio-n6j9t10q.js} +2 -2
  50. package/dist/chunks/{studio-68n67094.js → studio-q9jd1a60.js} +4 -4
  51. package/dist/chunks/studio-qcy05dp8.js +1743 -0
  52. package/dist/chunks/{studio-htsf0w6m.js → studio-qjy2z9bv.js} +12 -30
  53. package/dist/chunks/{studio-2zkz8evn.js → studio-trxrabqf.js} +12 -12
  54. package/dist/chunks/{studio-5q69gfag.js → studio-x4qkdat5.js} +12 -11
  55. package/dist/chunks/studio-x813zp48.js +195 -0
  56. package/dist/chunks/{studio-1hbqnt19.js → studio-zqazsct2.js} +29 -29
  57. package/dist/chunks/{workspace-xpres40v.js → workspace-fjx52ryv.js} +4 -4
  58. package/dist/iframe-entry.js +34 -33
  59. package/dist/manifest.json +7 -0
  60. package/dist/studio.js +534 -546
  61. package/favicon.ico +0 -0
  62. package/index.html +1 -0
  63. package/package.json +15 -14
  64. package/src/about/about-modal.ts +4 -3
  65. package/src/files/files.ts +6 -4
  66. package/src/files/media-paths.ts +13 -42
  67. package/src/files/media-usage.ts +41 -131
  68. package/src/hosting/document.ts +2 -0
  69. package/src/hosting/layout.ts +8 -0
  70. package/src/new-project/import-tab.ts +37 -15
  71. package/src/new-project/location-fields.ts +8 -5
  72. package/src/panels/ai-chat/composer.ts +10 -0
  73. package/src/platforms/cloud.ts +241 -70
  74. package/src/publish/publish-panel.ts +62 -2
  75. package/src/services/ai-import-tools.ts +50 -12
  76. package/src/services/ai-models.ts +108 -1
  77. package/src/services/context-manager.ts +12 -1
  78. package/src/services/tool-executor.ts +11 -0
  79. package/src/settings/preferences-accounts.ts +193 -12
  80. package/src/settings/preferences-dialog.ts +75 -18
  81. package/src/types.ts +27 -3
  82. package/src/ui/ai-managed-connect.ts +62 -13
  83. package/src/ui/ai-model-picker.ts +29 -2
  84. package/src/ui/cf-account-picker.ts +200 -0
  85. package/styles/shell.css +36 -0
  86. package/dist/chunks/studio-bw5dz6km.js +0 -608
@@ -25,7 +25,9 @@ import {
25
25
  isManagedProxy,
26
26
  isProxyConfigured,
27
27
  proxyStateCode,
28
+ resetModelCache,
28
29
  } from "../services/ai-models";
30
+ import type { CfConnectOutcome } from "../types";
29
31
 
30
32
  export interface ManagedConnectOptions {
31
33
  /** Host re-render scheduler — called on connect start/finish and when the probe settles. */
@@ -52,20 +54,73 @@ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnec
52
54
  let connectError = "";
53
55
 
54
56
  /*
55
- * Offer the keyless path when the proxy says managed-but-unconfigured and the platform can run
56
- * the hosted OAuth flow (the PAL seam — desktop shells can implement cfConnect later). Guarded on
57
- * hasPlatform() because gates can render from modules that load before registration.
57
+ * Offer the keyless path when the proxy is managed and the platform can run the hosted OAuth flow
58
+ * (the PAL seam — desktop shells can implement cfConnect later). Guarded on hasPlatform() because
59
+ * gates can render from modules that load before registration.
60
+ *
61
+ * `configured` alone does NOT withdraw the offer any more. A backend can answer `configured: true`
62
+ * over a grant it cannot actually use, and hiding the CTA there left the user with a broken
63
+ * assistant and no button at all — the one state in which reconnecting is the whole fix. So the
64
+ * lapsed code overrides `configured`, while `cf_upstream_error` (also `configured: true`, and
65
+ * carrying no reconnect code) still hides it: an unreachable Cloudflare is not fixed by an OAuth
66
+ * round trip.
58
67
  */
59
68
  function canOffer(): boolean {
60
- return (
61
- isManagedProxy() && !isProxyConfigured() && hasPlatform() && Boolean(getPlatform().cfConnect)
62
- );
69
+ if (!isManagedProxy() || !hasPlatform() || !getPlatform().cfConnect) {
70
+ return false;
71
+ }
72
+ return !isProxyConfigured() || proxyStateCode() === "cf_reconnect_required";
63
73
  }
64
74
 
65
75
  function ensureProbe() {
66
76
  ensureProxyProbe(opts.requestRender);
67
77
  }
68
78
 
79
+ /**
80
+ * Act on how the flow ended.
81
+ *
82
+ * Every branch here exists because the old code had ONE: any truthy result was success, so a
83
+ * connect that landed on the same lapsed row re-probed, got the same `cf_reconnect_required`
84
+ * back, and re-rendered a byte-identical CTA. The user clicked Reconnect and observably nothing
85
+ * happened — which is the half of the outage that made the other half impossible to diagnose.
86
+ *
87
+ * @param {CfConnectOutcome | null} outcome - Null where the platform had no DOM to open a popup
88
+ * in.
89
+ */
90
+ async function settle(outcome: CfConnectOutcome | null): Promise<void> {
91
+ /* A blocked popup turns into a full-page redirect, and null is a host with nowhere to open one:
92
+ in both the document is on its way out, so an apology would be the last thing painted before
93
+ it goes. Cancellation is silent for the opposite reason — the user already knows. */
94
+ if (!outcome || outcome.status === "redirect" || outcome.status === "canceled") {
95
+ return;
96
+ }
97
+ if (outcome.status === "timeout") {
98
+ connectError =
99
+ "The Cloudflare window didn't finish. Sign in there, then reconnect — nothing was changed.";
100
+ return;
101
+ }
102
+ if (!outcome.connection.accountId) {
103
+ /* Lazily, because the picker pulls the dialog layer in and this module is imported by every
104
+ credentials gate — including ones that render before layers are bound. */
105
+ const { openCfAccountPicker } = await import("./cf-account-picker");
106
+ if (!(await openCfAccountPicker())) {
107
+ connectError =
108
+ "Cloudflare is connected, but no account is chosen yet — pick one to finish setting up the assistant.";
109
+ return;
110
+ }
111
+ }
112
+ // Re-probe: /models flips to configured once the connection lands, opening the gate.
113
+ resetModelCache();
114
+ await fetchAvailableModels({ force: true });
115
+ /* And then CHECK, because a connect the backend does not honour must not look like one that
116
+ worked. This is the only place that can tell the difference. */
117
+ if (!isProxyConfigured() || proxyStateCode() === "cf_reconnect_required") {
118
+ connectError =
119
+ "Cloudflare connected, but the assistant backend still reports the connection as unusable. " +
120
+ "Try reconnecting, or use your own API key below.";
121
+ }
122
+ }
123
+
69
124
  async function connect() {
70
125
  if (busy) {
71
126
  return;
@@ -74,13 +129,7 @@ export function createManagedConnect(opts: ManagedConnectOptions): ManagedConnec
74
129
  connectError = "";
75
130
  opts.requestRender();
76
131
  try {
77
- const connection = await getPlatform().cfConnect?.();
78
- if (connection) {
79
- // Re-probe: /models flips to configured once the connection lands, opening the gate.
80
- await fetchAvailableModels({ force: true });
81
- } else {
82
- connectError = "Cloudflare connection was not completed.";
83
- }
132
+ await settle((await getPlatform().cfConnect?.()) ?? null);
84
133
  } catch (error) {
85
134
  connectError = error instanceof Error ? error.message : String(error);
86
135
  }
@@ -26,6 +26,7 @@ import {
26
26
  aiConnection,
27
27
  cachedModels,
28
28
  fetchAvailableModels,
29
+ modelToolSupport,
29
30
  preferredModel,
30
31
  } from "../services/ai-models";
31
32
  import { setModel } from "../services/ai-settings";
@@ -39,6 +40,21 @@ export const RETRY_MODELS = "__retry_models__";
39
40
  /** The `sp-menu-item` value of the disabled placeholder shown during the first fetch. */
40
41
  export const LOADING_MODELS = "__loading__";
41
42
 
43
+ /** What a listed model's row says when the backend reported it cannot call tools. */
44
+ export const NO_TOOLS_SUFFIX = " — no tools";
45
+
46
+ /**
47
+ * A model's row label: its name, plus a note when it is known to be chat-only.
48
+ *
49
+ * Labelled, never filtered or disabled. Choosing a chat-only model to ask questions of is a
50
+ * legitimate thing to do, and a picker that hides half a managed catalogue would be reporting a
51
+ * capability gap as an outage. Only `toolSupport === false` is labelled — `undefined` is a BYOK
52
+ * provider saying nothing, and a suffix there would be an invention.
53
+ */
54
+ function itemLabel(model: { id: string; name: string }): string {
55
+ return modelToolSupport(model.id) === false ? `${model.name}${NO_TOOLS_SUFFIX}` : model.name;
56
+ }
57
+
42
58
  export interface ModelPickerOptions {
43
59
  /** Host re-render scheduler — called whenever a fetch settles. */
44
60
  requestRender: () => void;
@@ -66,6 +82,12 @@ export interface ModelPicker {
66
82
  isLoading: () => boolean;
67
83
  /** The last fetch's failure, or `""`. */
68
84
  error: () => string;
85
+ /**
86
+ * Whether the selected model is known NOT to support tools — for a host that wants to say so.
87
+ *
88
+ * `false` while the backend has no opinion, so a host reads it as "warn" rather than as "block".
89
+ */
90
+ selectedLacksTools: () => boolean;
69
91
  }
70
92
 
71
93
  /**
@@ -143,7 +165,7 @@ export function createModelPicker(opts: ModelPickerOptions): ModelPicker {
143
165
  .value=${live(current)}
144
166
  @change=${onPickerChange}
145
167
  >
146
- ${items.map((m) => html`<sp-menu-item value=${m.id}>${m.name}</sp-menu-item>`)}
168
+ ${items.map((m) => html`<sp-menu-item value=${m.id}>${itemLabel(m)}</sp-menu-item>`)}
147
169
  ${
148
170
  loading
149
171
  ? html`<sp-menu-item disabled value=${LOADING_MODELS}>Loading models…</sp-menu-item>`
@@ -158,5 +180,10 @@ export function createModelPicker(opts: ModelPickerOptions): ModelPicker {
158
180
  `;
159
181
  }
160
182
 
161
- return { error: () => failure, isLoading: () => loading, render };
183
+ return {
184
+ error: () => failure,
185
+ isLoading: () => loading,
186
+ render,
187
+ selectedLacksTools: () => modelToolSupport(getModel()) === false,
188
+ };
162
189
  }
@@ -0,0 +1,200 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Cf-account-picker.ts — "which Cloudflare account?", the question nothing in Studio asked.
4
+ *
5
+ * A Cloudflare grant can reach several accounts, and the broker stores exactly one id per
6
+ * connection. Until one is chosen every Cloudflare-backed call answers `cf_account_required`, so a
7
+ * multi-account user finished the hosted OAuth flow and landed in a state that reads as connected
8
+ * and behaves as broken: the assistant 401s, publishing 401s, and no surface says why. The broker
9
+ * has had `/cf/accounts` and `/cf/select-account` the whole time; this is the first caller.
10
+ *
11
+ * ONE picker, invoked wherever a connect resolves account-less — the assistant's managed-connect
12
+ * CTA, the publish panel, and a button on the Preferences row. A second picker per surface is how
13
+ * three surfaces end up disagreeing about what "connected" means.
14
+ *
15
+ * Cloud-only by construction: the PAL members it drives are the brokered ones, so a platform
16
+ * holding a pasted API token (desktop, dev server) keeps its own local account field and this
17
+ * resolves null without opening anything.
18
+ *
19
+ * @docs studio/ai
20
+ * @license MIT
21
+ */
22
+
23
+ import { html, render as litRender, nothing } from "lit-html";
24
+ import { ref } from "lit-html/directives/ref.js";
25
+ import type { TemplateResult } from "lit-html";
26
+ import { getPlatform, hasPlatform } from "../platform";
27
+ import { notifyCredentialsChanged } from "../settings/preferences-accounts";
28
+ import { showDialog } from "./layers";
29
+ import { overlayRegion } from "./regions";
30
+ import type { CfAccountSummary } from "../types";
31
+
32
+ /** Whether this platform brokers the connection, and so can be asked which accounts it reaches. */
33
+ function canPick(): boolean {
34
+ if (!hasPlatform()) {
35
+ return false;
36
+ }
37
+ const platform = getPlatform();
38
+ return Boolean(platform.cfAccounts && platform.cfSelectAccount);
39
+ }
40
+
41
+ /**
42
+ * Ask which Cloudflare account this connection should use, and store the answer.
43
+ *
44
+ * Resolves the chosen account, or null — dismissed, or a platform that does not broker connections
45
+ * at all. A caller treats null as "still unusable" rather than as a failure: nothing was changed,
46
+ * and the surface that opened this is the one that knows what to say about it.
47
+ *
48
+ * @returns {Promise<CfAccountSummary | null>}
49
+ */
50
+ export function openCfAccountPicker(): Promise<CfAccountSummary | null> {
51
+ if (!canPick()) {
52
+ return Promise.resolve(null);
53
+ }
54
+ return showDialog<CfAccountSummary | null>(
55
+ (done) => {
56
+ let accounts: CfAccountSummary[] = [];
57
+ let loading = true;
58
+ /** The last refusal — a listing that failed, or an account the broker would not accept. */
59
+ let failure = "";
60
+ /** The id being committed, so its own row can say so rather than the whole list going grey. */
61
+ let choosing = "";
62
+ let settled = false;
63
+ let wrapperEl: HTMLElement | null = null;
64
+
65
+ function finish(account: CfAccountSummary | null): void {
66
+ settled = true;
67
+ done(account);
68
+ }
69
+
70
+ function repaint(): void {
71
+ /* Resolved lazily, and only while the dialog is still up: `done` releases the slot, so a
72
+ late `load()` landing after a dismissal would otherwise paint into a detached host. */
73
+ const host = settled ? null : wrapperEl?.parentElement;
74
+ if (host) {
75
+ litRender(build(), host);
76
+ }
77
+ }
78
+
79
+ async function load(): Promise<void> {
80
+ loading = true;
81
+ failure = "";
82
+ repaint();
83
+ try {
84
+ accounts = (await getPlatform().cfAccounts?.()) ?? [];
85
+ } catch (error) {
86
+ failure = error instanceof Error ? error.message : String(error);
87
+ }
88
+ loading = false;
89
+ repaint();
90
+ }
91
+
92
+ async function choose(account: CfAccountSummary): Promise<void> {
93
+ choosing = account.id;
94
+ failure = "";
95
+ repaint();
96
+ try {
97
+ await getPlatform().cfSelectAccount?.({ id: account.id, name: account.name });
98
+ } catch (error) {
99
+ failure = error instanceof Error ? error.message : String(error);
100
+ choosing = "";
101
+ repaint();
102
+ return;
103
+ }
104
+ /* The stored account is a credential like any other: the assistant's gate, the publish
105
+ panel and the Preferences row all re-read from the same announcement. */
106
+ notifyCredentialsChanged();
107
+ finish(account);
108
+ }
109
+
110
+ function listTpl(): TemplateResult | typeof nothing {
111
+ if (loading) {
112
+ return html`<p class="cf-account-picker-empty">Reading your Cloudflare accounts…</p>`;
113
+ }
114
+ /* Nothing to show and a reason why. The retry is offered only here: a commit that was
115
+ refused (below) keeps the list, because listing again is not what would fix it. */
116
+ if (accounts.length === 0) {
117
+ return html`
118
+ <p class="cf-account-picker-empty">
119
+ ${
120
+ failure
121
+ ? `Cloudflare could not be reached: ${failure}`
122
+ : "This Cloudflare login reaches no accounts. Create one at dash.cloudflare.com, then try again."
123
+ }
124
+ </p>
125
+ ${
126
+ failure
127
+ ? html`<sp-button
128
+ size="s"
129
+ treatment="outline"
130
+ @click=${() => {
131
+ void load();
132
+ }}
133
+ >
134
+ Try again
135
+ </sp-button>`
136
+ : nothing
137
+ }
138
+ `;
139
+ }
140
+ /* The row shape is Preferences › Accounts', deliberately reused: this IS an account list,
141
+ and a second stylesheet entry for the same shape is how two lists drift apart. */
142
+ return html`
143
+ ${failure ? html`<p class="cf-account-picker-error">${failure}</p>` : nothing}
144
+ <div class="prefs-accounts">
145
+ ${accounts.map(
146
+ (account) => html`
147
+ <div class="prefs-account" data-account=${account.id}>
148
+ <div class="prefs-account-text">
149
+ <span class="prefs-account-label">${account.name}</span>
150
+ <span class="prefs-account-detail">${account.id}</span>
151
+ </div>
152
+ <sp-button
153
+ size="s"
154
+ variant="accent"
155
+ ?disabled=${choosing !== ""}
156
+ @click=${() => {
157
+ void choose(account);
158
+ }}
159
+ >
160
+ ${choosing === account.id ? "Selecting…" : "Use this account"}
161
+ </sp-button>
162
+ </div>
163
+ `,
164
+ )}
165
+ </div>
166
+ `;
167
+ }
168
+
169
+ function build(): TemplateResult {
170
+ return html`
171
+ <sp-dialog-wrapper
172
+ open
173
+ underlay
174
+ headline="Choose a Cloudflare account"
175
+ cancel-label="Cancel"
176
+ @cancel=${() => finish(null)}
177
+ @close=${() => finish(null)}
178
+ ${ref((el?: Element) => {
179
+ if (el) {
180
+ wrapperEl = el as HTMLElement;
181
+ }
182
+ })}
183
+ >
184
+ <div class="cf-account-picker">
185
+ <p class="cf-account-picker-lede">
186
+ Your Cloudflare login reaches more than one account. Publishing and the assistant
187
+ both run against the one you pick; you can change it in Preferences › Accounts.
188
+ </p>
189
+ ${listTpl()}
190
+ </div>
191
+ </sp-dialog-wrapper>
192
+ `;
193
+ }
194
+
195
+ void load();
196
+ return build();
197
+ },
198
+ { region: overlayRegion("dialog", "cf-accounts") },
199
+ );
200
+ }
package/styles/shell.css CHANGED
@@ -800,6 +800,13 @@
800
800
  max-width: 140px;
801
801
  min-width: 0;
802
802
  }
803
+ /* The chat-only-model note, under the picker that caused it. Advisory weight on purpose: the
804
+ assistant still answers, it just cannot edit. */
805
+ .ai-composer-note {
806
+ color: var(--fg-dim);
807
+ font-size: var(--spectrum-font-size-50, 11px);
808
+ line-height: 1.3;
809
+ }
803
810
  /* Sessions list */
804
811
  .ai-sessions {
805
812
  flex: 1 1 0;
@@ -1375,6 +1382,35 @@
1375
1382
  font-size: var(--spectrum-font-size-50, 11px);
1376
1383
  color: var(--fg-dim);
1377
1384
  }
1385
+ /* The account picker borrows `.prefs-account` for its rows on purpose — it is the same object in a
1386
+ different frame, and a second row style would drift from this one. Only its own chrome is here. */
1387
+ .cf-account-picker {
1388
+ display: flex;
1389
+ flex-direction: column;
1390
+ gap: 12px;
1391
+ min-width: 320px;
1392
+ }
1393
+ .cf-account-picker-lede {
1394
+ margin: 0;
1395
+ color: var(--fg-dim);
1396
+ font-size: var(--spectrum-font-size-75, 12px);
1397
+ }
1398
+ .cf-account-picker-empty {
1399
+ margin: 0;
1400
+ color: var(--fg-dim);
1401
+ font-size: var(--spectrum-font-size-75, 12px);
1402
+ }
1403
+ .cf-account-picker-error {
1404
+ margin: 0;
1405
+ color: var(--danger);
1406
+ font-size: var(--spectrum-font-size-75, 12px);
1407
+ }
1408
+ .prefs-account-actions {
1409
+ display: flex;
1410
+ align-items: center;
1411
+ gap: 8px;
1412
+ flex-shrink: 0;
1413
+ }
1378
1414
  .prefs-keys {
1379
1415
  display: flex;
1380
1416
  flex-direction: column;