dsh-subscriptions 0.2.2 → 0.2.3

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.
package/README.md CHANGED
@@ -77,10 +77,17 @@ the composer's tool row: Standard or Fast, per session.
77
77
  ![Speed toggle with the Standard/Fast menu open](docs/images/speed-toggle.png)
78
78
 
79
79
  **Settings → Subscriptions → provider → Manage** edits account aliases, pool
80
- participation, model allowlists, and the per-model effort, context, and tool
81
- settings. A context value only lowers the window a session plans against, never
82
- above what the provider advertises; Codex and Claude support it, and it does not
83
- enlarge the API's capacity.
80
+ participation, model allowlists, and the per-model effort, subagent speed,
81
+ context, and tool settings. A context value only lowers the window a session
82
+ plans against, never above what the provider advertises; Codex and Claude
83
+ support it, and it does not enlarge the API's capacity.
84
+
85
+ For Codex, **Subagent default speed** applies to subsequent subagent requests
86
+ using that model, regardless of the parent model. An explicit Speed choice in
87
+ the subagent session overrides the saved default, and a parent's choice never
88
+ propagates. The default survives restart; session overrides do not. Fast still
89
+ requires current model support, and requests without live subagent metadata use
90
+ Standard.
84
91
 
85
92
  ![Model settings](docs/images/model-settings.png)
86
93
 
@@ -154,15 +161,16 @@ text. No live account was used to verify what Anthropic returns. See
154
161
 
155
162
  ## Install
156
163
 
157
- Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.2`, and `0.2.1-rc.2`.
158
- The package's peers admit `>=0.1.7-rc.2 <0.3.0-0`: untested versions inside that
159
- range install but are unsupported, and versions outside it need an explicit
160
- `dsh plugin allow-version` exemption. `dsh plugin add` checks those ranges with
161
- `includePrerelease` semantics, so the admitted set covers every host build from
162
- `0.1.7-rc.2` through the whole `0.2.x` line, prereleases included, and blocks
163
- the `0.3.0` line. See [docs/compatibility.md](docs/compatibility.md) for the
164
- support window and [docs/testing.md](docs/testing.md) for the test layers. With
165
- `dsh` available, install the plugin into the web profile from npm:
164
+ Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.2`, `0.2.1-rc.2`, and
165
+ `0.2.1-rc.5`. The package's peers admit `>=0.1.7-rc.2 <0.3.0-0`: untested
166
+ versions inside that range install but are unsupported, and versions outside it
167
+ need an explicit `dsh plugin allow-version` exemption. `dsh plugin add` checks
168
+ those ranges with `includePrerelease` semantics, so the admitted set covers
169
+ every host build from `0.1.7-rc.2` through the whole `0.2.x` line, prereleases
170
+ included, and blocks the `0.3.0` line. See
171
+ [docs/compatibility.md](docs/compatibility.md) for the support window and
172
+ [docs/testing.md](docs/testing.md) for the test layers. With `dsh` available,
173
+ install the plugin into the web profile from npm:
166
174
 
167
175
  ```sh
168
176
  dsh plugin --profile web add dsh-subscriptions
package/lib/auth/rpc.d.ts CHANGED
@@ -38,15 +38,15 @@ export interface VideoBytesResult {
38
38
  export type SpeedTier = 'standard' | 'fast';
39
39
  /** `speed` endpoint value: the session's choice plus the visibility list. */
40
40
  export interface SpeedState {
41
- /** The session's current speed tier (default `standard`). */
41
+ /** Explicit session speed, or the subagent's model default when supplied. */
42
42
  tier: SpeedTier;
43
43
  /** Codex model ids whose catalog advertises a fast tier. */
44
44
  fastModels: string[];
45
45
  }
46
- /** Speed state the RPC handler delegates to (in-memory, per session). */
46
+ /** Session overrides plus persisted subagent model defaults. */
47
47
  export interface SpeedController {
48
- /** Current speed state: the session's tier and the fast-capable codex models. */
49
- speed(sessionId: string): Promise<SpeedState>;
48
+ /** Pass the current Codex model to resolve a subagent's default speed. */
49
+ speed(sessionId: string, model?: string): Promise<SpeedState>;
50
50
  /** Set one session's speed tier. */
51
51
  setSpeed(sessionId: string, tier: SpeedTier): Promise<void>;
52
52
  }
@@ -65,6 +65,8 @@ export interface AccountStatus {
65
65
  }
66
66
  /** Login state of one provider, as rendered by the Settings page. */
67
67
  export interface ProviderStatus {
68
+ /** Saved picker visibility, attached by the status RPC from local preferences. */
69
+ visibleModels?: string[];
68
70
  /** Whether a login attempt is currently waiting for its code. */
69
71
  busy: boolean;
70
72
  /** Logged-in accounts, default first. */
@@ -110,6 +112,8 @@ export interface ProviderAccountCatalog {
110
112
  }
111
113
  /** Provider and account preference operations behind providerSettings/setProviderSettings. */
112
114
  export interface ProviderSettingsController {
115
+ /** Local picker selection, without triggering model discovery. */
116
+ visibleModels?(provider: ProviderId): string[] | undefined;
113
117
  get(provider: ProviderId, force: boolean): Promise<unknown>;
114
118
  set(provider: ProviderId, settings: unknown): Promise<void>;
115
119
  }
package/lib/auth/rpc.js CHANGED
@@ -309,7 +309,18 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
309
309
  detail: error instanceof Error ? error.message : String(error),
310
310
  })),
311
311
  ]));
312
- return ok({ providers: Object.fromEntries(entries) });
312
+ return ok({
313
+ providers: Object.fromEntries(entries.map(([provider, status]) => {
314
+ const visibleModels = providerSettings?.visibleModels?.(provider);
315
+ return [
316
+ provider,
317
+ {
318
+ ...status,
319
+ ...(visibleModels === undefined ? {} : { visibleModels }),
320
+ },
321
+ ];
322
+ })),
323
+ });
313
324
  }
314
325
  case 'login': {
315
326
  const provider = readProvider(payload);
@@ -433,7 +444,9 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
433
444
  case 'video':
434
445
  return ok(await controller.readVideo(readVideoName(payload), signal));
435
446
  case 'speed':
436
- return ok(await speed.speed(readSessionId(payload)));
447
+ return ok(await speed.speed(readSessionId(payload), payload.model === undefined
448
+ ? undefined
449
+ : readString(payload, 'model')));
437
450
  case 'setSpeed':
438
451
  await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
439
452
  return ok({ ok: true });
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
+ import { modelEditorState, savedModelIds } from './model-filter.js';
4
5
  /** Providers whose runtime honours a local context-window override. */
5
6
  const supportsContext = (provider) => provider === 'codex' || provider === 'claude';
6
7
  const border = '0.5px solid var(--dsw-alias-border-l2, #ddd)';
@@ -108,18 +109,26 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
108
109
  }), [catalog, dirty, draft, contexts, efforts, provider, t]);
109
110
  const allModels = catalog?.models ?? [];
110
111
  const known = new Set(allModels.map((model) => model.id));
111
- const missing = (draft.visibleModels ?? [])
112
+ const savedIds = savedModelIds(draft.visibleModels, draft.subagentSpeeds);
113
+ const missing = savedIds
112
114
  .filter((id) => !known.has(id))
113
115
  .map((id) => ({ id, name: id }));
114
116
  const models = [...allModels, ...missing].filter((model) => `${model.name} ${model.id}`
115
117
  .toLowerCase()
116
118
  .includes(query.trim().toLowerCase()));
117
- const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
119
+ const editorState = modelEditorState({
120
+ discovered: allModels.length,
121
+ saved: missing.length,
122
+ matched: models.length,
123
+ accounts: catalog?.accounts,
124
+ });
125
+ const selected = new Set(draft.visibleModels ??
126
+ [...allModels, ...missing].map((model) => model.id));
118
127
  return (_jsxs("div", { style: { borderTop: border, marginTop: 12, paddingTop: 12 }, children: [_jsx("h3", { style: { margin: 0 }, children: t('modelsEdit') }), _jsxs("div", { style: { display: 'grid', gap: 12, marginTop: 12 }, children: [_jsx("p", { className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription'
119
128
  ? 'cursorManageHint'
120
129
  : 'modelsHint') }), error && _jsx("p", { role: "alert", children: error }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busy || disabled || dirty, onClick: () => {
121
130
  void load(true);
122
- }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), catalog && (_jsxs("fieldset", { disabled: busy || disabled, style: {
131
+ }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), !busy && catalog && (_jsxs("fieldset", { disabled: busy || disabled, style: {
123
132
  border: 0,
124
133
  padding: 0,
125
134
  margin: 0,
@@ -133,44 +142,57 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
133
142
  else
134
143
  next.visibleModels = allModels.map((model) => model.id);
135
144
  edit(next);
136
- } }), ' ', t('modelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("input", { style: { flex: '1 1 180px' }, value: query, onChange: (event) => setQuery(event.target.value), placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder') }), _jsx("button", { type: "button", onClick: () => edit({
137
- ...draft,
138
- visibleModels: allModels.map((model) => model.id),
139
- }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit({ ...draft, visibleModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: {
140
- maxHeight: 360,
141
- overflowY: 'auto',
142
- display: 'grid',
143
- gap: 8,
144
- }, children: [models.map((model) => (_jsxs("div", { style: {
145
- borderBottom: border,
146
- padding: '8px 2px',
145
+ } }), ' ', t('modelsAutomatic')] }), editorState.listState === 'provider-empty' ? (_jsx("p", { className: "dsh-subscription-hint", children: t(editorState.noticeKey) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: actions, children: [_jsx("input", { style: { flex: '1 1 180px' }, value: query, onChange: (event) => setQuery(event.target.value), placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder') }), _jsx("button", { type: "button", onClick: () => edit({
146
+ ...draft,
147
+ visibleModels: allModels.map((model) => model.id),
148
+ }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit({ ...draft, visibleModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: {
149
+ maxHeight: 360,
150
+ overflowY: 'auto',
147
151
  display: 'grid',
148
152
  gap: 8,
149
- }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: (event) => {
150
- const next = new Set(selected);
151
- if (event.target.checked)
152
- next.add(model.id);
153
- else
154
- next.delete(model.id);
155
- edit({ ...draft, visibleModels: [...next] });
156
- } }), ' ', model.name, !known.has(model.id) && ` (${t('modelsUnavailable')})`] }), _jsxs("div", { style: actions, children: [(model.efforts?.length ?? 0) > 0 && (_jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { "aria-label": `${model.name} ${t('modelDefaultsTitle')}`, value: efforts[model.id] ?? '', onChange: (event) => {
157
- setEfforts({
158
- ...efforts,
159
- [model.id]: event.target.value,
160
- });
161
- setDirty(true);
162
- }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured &&
163
- !model.efforts?.some((effort) => effort.id === model.configured) && (_jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] })), model.efforts?.map((effort) => (_jsx("option", { value: effort.id, children: effort.name }, effort.id)))] })] })), supportsContext(provider) &&
164
- model.maxContextWindow !== undefined && (_jsxs("div", { style: actions, children: [_jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", value: contexts[model.id] ?? '', "aria-label": `${model.name} ${t('modelsContext')}`, placeholder: String(model.defaultContextWindow), onChange: (event) => {
165
- setContexts({
166
- ...contexts,
153
+ }, children: [models.map((model) => (_jsxs("div", { style: {
154
+ borderBottom: border,
155
+ padding: '8px 2px',
156
+ display: 'grid',
157
+ gap: 8,
158
+ }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: (event) => {
159
+ const next = new Set(selected);
160
+ if (event.target.checked)
161
+ next.add(model.id);
162
+ else
163
+ next.delete(model.id);
164
+ edit({ ...draft, visibleModels: [...next] });
165
+ } }), ' ', model.name, !known.has(model.id) &&
166
+ ` (${t('modelsUnavailable')})`] }), _jsxs("div", { style: actions, children: [(model.efforts?.length ?? 0) > 0 && (_jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { "aria-label": `${model.name} ${t('modelDefaultsTitle')}`, value: efforts[model.id] ?? '', onChange: (event) => {
167
+ setEfforts({
168
+ ...efforts,
167
169
  [model.id]: event.target.value,
168
170
  });
169
171
  setDirty(true);
170
- } })] }), _jsx("small", { children: t('modelsContextBounds', {
171
- default: model.defaultContextWindow,
172
- max: model.maxContextWindow,
173
- }) })] }))] })] }, model.id))), models.length === 0 && (_jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) }))] }), supportsContext(provider) && (_jsx("small", { children: t('modelsContextHint') })), catalog.tools.length > 0 && (_jsxs("div", { style: { display: 'grid', gap: 8 }, children: [_jsx("strong", { children: t('modelsTools') }), _jsx("small", { children: t('modelsToolsHint') }), catalog.tools.map((tool) => (_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.tools?.[tool] !== false, onChange: (event) => edit({
172
+ }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured &&
173
+ !model.efforts?.some((effort) => effort.id === model.configured) && (_jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] })), model.efforts?.map((effort) => (_jsx("option", { value: effort.id, children: effort.name }, effort.id)))] })] })), supportsContext(provider) &&
174
+ model.maxContextWindow !== undefined && (_jsxs("div", { style: actions, children: [_jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", value: contexts[model.id] ?? '', "aria-label": `${model.name} ${t('modelsContext')}`, placeholder: String(model.defaultContextWindow), onChange: (event) => {
175
+ setContexts({
176
+ ...contexts,
177
+ [model.id]: event.target.value,
178
+ });
179
+ setDirty(true);
180
+ } })] }), _jsx("small", { children: t('modelsContextBounds', {
181
+ default: model.defaultContextWindow,
182
+ max: model.maxContextWindow,
183
+ }) })] }))] }), provider === 'codex' &&
184
+ (model.fastTier === true ||
185
+ Object.hasOwn(draft.subagentSpeeds ?? {}, model.id)) && (_jsxs("div", { style: { display: 'grid', gap: 6 }, children: [_jsxs("label", { style: actions, children: [t('modelsSubagentSpeed'), _jsxs("select", { "aria-label": `${model.name} ${t('modelsSubagentSpeed')}`, value: Object.hasOwn(draft.subagentSpeeds ?? {}, model.id)
186
+ ? draft.subagentSpeeds[model.id]
187
+ : 'standard', onChange: (event) => edit({
188
+ ...draft,
189
+ subagentSpeeds: {
190
+ ...draft.subagentSpeeds,
191
+ [model.id]: event.target
192
+ .value,
193
+ },
194
+ }), children: [_jsx("option", { value: "standard", children: t('speedStandard') }), _jsxs("option", { value: "fast", disabled: !model.fastTier, children: [t('speedFast'), !model.fastTier &&
195
+ ` (${t('modelsUnavailable')})`] })] })] }), _jsx("small", { children: t('modelsSubagentSpeedHint') })] }))] }, model.id))), models.length === 0 && (_jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) }))] })] })), supportsContext(provider) && (_jsx("small", { children: t('modelsContextHint') })), catalog.tools.length > 0 && (_jsxs("div", { style: { display: 'grid', gap: 8 }, children: [_jsx("strong", { children: t('modelsTools') }), _jsx("small", { children: t('modelsToolsHint') }), catalog.tools.map((tool) => (_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.tools?.[tool] !== false, onChange: (event) => edit({
174
196
  ...draft,
175
197
  tools: {
176
198
  ...draft.tools,
@@ -32,20 +32,21 @@ import { en } from './locales.js';
32
32
  */
33
33
  export function createSpeedLoader(connection, models, sessionId) {
34
34
  return async () => {
35
- const state = await callSubscriptionsAuth(connection.rpc, 'speed', { sessionId });
36
35
  const directories = models();
37
- if (directories === undefined)
38
- return { visible: false, tier: state.tier };
39
- let current;
40
- try {
41
- const loaded = await directories.directoryFor(sessionId).load();
42
- current = loaded.current;
43
- }
44
- catch (error) {
45
- if (!modelSelectionUnavailable(error))
46
- throw error;
47
- return { visible: false, tier: state.tier };
36
+ let current = null;
37
+ if (directories !== undefined) {
38
+ try {
39
+ current = (await directories.directoryFor(sessionId).load()).current;
40
+ }
41
+ catch (error) {
42
+ if (!modelSelectionUnavailable(error))
43
+ throw error;
44
+ }
48
45
  }
46
+ const state = await callSubscriptionsAuth(connection.rpc, 'speed', {
47
+ sessionId,
48
+ ...(current?.provider === 'codex' ? { model: current.model } : {}),
49
+ });
49
50
  const visible = current !== null &&
50
51
  current.provider === 'codex' &&
51
52
  state.fastModels.includes(current.model);
@@ -13,6 +13,8 @@ export interface AccountStatus {
13
13
  }
14
14
  /** One provider's login state as answered by the `status` endpoint. */
15
15
  export interface ProviderStatus {
16
+ /** Picker selection; absent follows discovery, [] hides all model quotas. */
17
+ visibleModels?: string[];
16
18
  busy: boolean;
17
19
  accounts: AccountStatus[];
18
20
  detail?: string;
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  import { useCallback, useEffect, useRef, useState } from 'react';
16
16
  import { en } from './locales.js';
17
17
  import { resetActionBlock } from './reset-action.js';
18
+ import { visibleUsageWindows } from './usage-window-filter.js';
18
19
  import { ProviderAccountManager } from './ProviderAccountManager.js';
19
20
  import { providerSettingsCss } from './provider-settings-styles.js';
20
21
  import { ExternalUsageCards } from './ExternalUsageCards.js';
@@ -641,6 +642,7 @@ export function SubscriptionsSection(props) {
641
642
  return (_jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) } }), _jsx("span", { style: styles.name, children: name }), status?.clientVersion !== undefined && (_jsx("span", { style: styles.clientVersion, title: t('clientVersionHint'), children: clientVersionText(t, status.clientVersion) }))] }), _jsx("p", { style: styles.statusLine, children: statusText(t, status) }), status?.detail !== undefined && status.detail !== '' && (_jsx("p", { style: styles.statusLine, children: status.detail })), errors[id] !== undefined && (_jsx("p", { style: styles.errorLine, children: errors[id] })), accounts.map((account) => {
642
643
  const usageKey = `${id}:${account.key}`;
643
644
  const usage = usages[usageKey];
645
+ const shownWindows = visibleUsageWindows(id, usage?.windows, status?.visibleModels);
644
646
  const usageError = usageErrors[usageKey];
645
647
  const display = account.account ?? account.key;
646
648
  const sortedCredits = [...(usage?.resetCredits ?? [])].sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity));
@@ -664,7 +666,9 @@ export function SubscriptionsSection(props) {
664
666
  }, children: t('logout') })] }), showUsage && (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: usageLoading[usageKey] === true, onClick: () => {
665
667
  void loadUsage(id, account.key, true);
666
668
  }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined &&
667
- usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
669
+ usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), usage?.windows !== undefined &&
670
+ usage.windows.length > 0 &&
671
+ shownWindows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNoVisibleModels') })), shownWindows.map((window, index) => {
668
672
  const percent = displayUsedPercent(window.usedPercent);
669
673
  return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [percent === undefined
670
674
  ? t('usageMeterInvalid')
@@ -702,7 +706,7 @@ export function SubscriptionsSection(props) {
702
706
  void login(id, 'keychain');
703
707
  }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", onClick: () => {
704
708
  void login(id);
705
- }, children: t('addAccount') })), _jsx("button", { type: "button", "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", onClick: () => {
709
+ }, children: t('addAccount') })), !busy && (_jsx("button", { type: "button", "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') })), busy && (_jsx("button", { type: "button", onClick: () => {
706
710
  void cancel(id);
707
711
  }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", onClick: () => {
708
712
  copyDeviceCode(id, deviceCode.userCode);
@@ -716,7 +720,10 @@ export function SubscriptionsSection(props) {
716
720
  })) }), _jsx("button", { type: "button", onClick: () => {
717
721
  void submitManual(id);
718
722
  }, children: t('submit') })] })] }))] }, id));
719
- }), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(OllamaCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && (_jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) })), _jsx("style", { children: '.subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }' }), _jsx("dialog", { ref: resetDialogRef, className: "subscriptions-reset-dialog", "aria-labelledby": "subscriptions-reset-title", onCancel: () => setResetConfirmation(undefined), onClose: () => setResetConfirmation(undefined), style: {
723
+ }), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(OllamaCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && (_jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => {
724
+ setManagedProvider(undefined);
725
+ void refresh();
726
+ } })), _jsx("style", { children: '.subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }' }), _jsx("dialog", { ref: resetDialogRef, className: "subscriptions-reset-dialog", "aria-labelledby": "subscriptions-reset-title", onCancel: () => setResetConfirmation(undefined), onClose: () => setResetConfirmation(undefined), style: {
720
727
  ...styles.modal,
721
728
  display: resetConfirmation ? 'flex' : 'none',
722
729
  margin: 'auto',
@@ -29,6 +29,8 @@ export declare const en: {
29
29
  modelsContextBounds: string;
30
30
  modelsContextHint: string;
31
31
  modelsContextInvalid: string;
32
+ modelsSubagentSpeed: string;
33
+ modelsSubagentSpeedHint: string;
32
34
  modelsTools: string;
33
35
  modelsToolsHint: string;
34
36
  modelsImage: string;
@@ -78,6 +80,7 @@ export declare const en: {
78
80
  usageRefresh: string;
79
81
  usageLoading: string;
80
82
  usageEmpty: string;
83
+ usageNoVisibleModels: string;
81
84
  usageUnsupported: string;
82
85
  usageError: string;
83
86
  usageSession: string;
@@ -176,6 +179,9 @@ export declare const en: {
176
179
  modelDefaultsSummaryEmpty: string;
177
180
  modelDefaultsFilterPlaceholder: string;
178
181
  modelDefaultsFilterEmpty: string;
182
+ modelDefaultsNoModels: string;
183
+ modelDefaultsNoAccount: string;
184
+ modelDefaultsDiscoveryFailed: string;
179
185
  generating: string;
180
186
  image: string;
181
187
  viewImage: string;
@@ -269,6 +275,8 @@ export declare const zh: {
269
275
  modelsContextBounds: string;
270
276
  modelsContextHint: string;
271
277
  modelsContextInvalid: string;
278
+ modelsSubagentSpeed: string;
279
+ modelsSubagentSpeedHint: string;
272
280
  modelsTools: string;
273
281
  modelsToolsHint: string;
274
282
  modelsImage: string;
@@ -318,6 +326,7 @@ export declare const zh: {
318
326
  usageRefresh: string;
319
327
  usageLoading: string;
320
328
  usageEmpty: string;
329
+ usageNoVisibleModels: string;
321
330
  usageUnsupported: string;
322
331
  usageError: string;
323
332
  usageSession: string;
@@ -416,6 +425,9 @@ export declare const zh: {
416
425
  modelDefaultsSummaryEmpty: string;
417
426
  modelDefaultsFilterPlaceholder: string;
418
427
  modelDefaultsFilterEmpty: string;
428
+ modelDefaultsNoModels: string;
429
+ modelDefaultsNoAccount: string;
430
+ modelDefaultsDiscoveryFailed: string;
419
431
  generating: string;
420
432
  image: string;
421
433
  viewImage: string;
@@ -21,7 +21,7 @@ export const en = {
21
21
  accountsCatalogUnavailable: 'Model discovery is currently unavailable. Saved model selections are preserved.',
22
22
  modelsPartialSave: 'Some reasoning defaults were saved. Remaining changes are still in the draft; retry to finish.',
23
23
  modelsEdit: 'Edit model list',
24
- modelsHint: 'Configure model visibility, default reasoning effort, context and tools here; Save changes below submits them together with the account settings. Hidden models remain usable by existing sessions; tool changes apply to new sessions.',
24
+ modelsHint: 'Configure model visibility, default reasoning effort, subagent speed, context and tools here; Save changes below submits them together with the account settings. Hidden models remain usable by existing sessions; tool changes apply to new sessions.',
25
25
  modelsAutomatic: 'Automatically show all models, including new models',
26
26
  modelsSelectAll: 'Select all',
27
27
  modelsSelectNone: 'Clear selection',
@@ -29,6 +29,8 @@ export const en = {
29
29
  modelsContextBounds: 'Default {default} · Maximum {max}',
30
30
  modelsContextHint: 'Leave blank to follow the provider. The requested window is capped by the account’s advertised maximum; account pools use the smallest member window. This controls local history and compaction, not the API’s capacity.',
31
31
  modelsContextInvalid: 'Enter a positive whole number of tokens for {model}.',
32
+ modelsSubagentSpeed: 'Subagent default speed',
33
+ modelsSubagentSpeedHint: 'Applies to subsequent subagent requests using this model, regardless of the parent model. Main conversations keep their own Speed setting. Fast requires model support.',
32
34
  modelsTools: 'Provider tools',
33
35
  modelsToolsHint: 'Changes apply only to sessions created after saving. Existing sessions keep their tools. Shared tools remain available if any supporting provider is enabled.',
34
36
  modelsImage: 'Image generation',
@@ -78,6 +80,7 @@ export const en = {
78
80
  usageRefresh: 'Refresh',
79
81
  usageLoading: 'Loading usage…',
80
82
  usageEmpty: 'No usage windows reported.',
83
+ usageNoVisibleModels: 'No usage windows for the models visible in the picker.',
81
84
  usageUnsupported: 'This endpoint has no usage surface.',
82
85
  usageError: 'Usage lookup failed: {message}',
83
86
  usageSession: '5-hour window',
@@ -176,6 +179,9 @@ export const en = {
176
179
  modelDefaultsSummaryEmpty: 'No model advertises reasoning levels',
177
180
  modelDefaultsFilterPlaceholder: 'Filter models',
178
181
  modelDefaultsFilterEmpty: 'No model matches “{query}”.',
182
+ modelDefaultsNoModels: 'This provider reports no models.',
183
+ modelDefaultsNoAccount: 'No connected accounts. Connect one on the provider card first.',
184
+ modelDefaultsDiscoveryFailed: 'Model discovery failed or timed out. Use Refresh to try again.',
179
185
  generating: 'Generating image…',
180
186
  image: 'image',
181
187
  viewImage: 'View image',
@@ -269,7 +275,7 @@ export const zh = {
269
275
  accountsCatalogUnavailable: '暂时无法获取模型目录,已保存的模型选择会被保留。',
270
276
  modelsPartialSave: '部分推理档已保存,其余更改仍保留在草稿中,请重试完成保存。',
271
277
  modelsEdit: '编辑模型列表',
272
- modelsHint: '在此配置模型显示、默认推理档、上下文与工具,底部的「保存更改」会连同账号设置一起提交。隐藏模型不影响已有会话;工具开关对新建会话生效。',
278
+ modelsHint: '在此配置模型显示、默认推理档、子代理速度、上下文与工具,底部的「保存更改」会连同账号设置一起提交。隐藏模型不影响已有会话;工具开关对新建会话生效。',
273
279
  modelsAutomatic: '自动显示全部模型(包含以后新增的模型)',
274
280
  modelsSelectAll: '全选',
275
281
  modelsSelectNone: '清空选择',
@@ -277,6 +283,8 @@ export const zh = {
277
283
  modelsContextBounds: '默认 {default} · 最大 {max}',
278
284
  modelsContextHint: '留空跟随服务商。配置值按账号返回的最大窗口限制;账号池取各成员的最小窗口。此设置控制本地历史保留和压缩时机,不会扩大 API 本身的容量。',
279
285
  modelsContextInvalid: '请为 {model} 输入正整数 token 数。',
286
+ modelsSubagentSpeed: '子代理默认速度',
287
+ modelsSubagentSpeedHint: '对使用此模型的子代理后续请求生效,与父会话模型无关。主会话仍使用自己的速度设置;快速档仅在模型支持时生效。',
280
288
  modelsTools: 'Provider 工具',
281
289
  modelsToolsHint: '仅对保存后新建的会话生效,已有会话保留原工具。任一支持的服务商启用共享工具时,该工具仍会显示。',
282
290
  modelsImage: '图片生成',
@@ -326,6 +334,7 @@ export const zh = {
326
334
  usageRefresh: '刷新',
327
335
  usageLoading: '用量加载中…',
328
336
  usageEmpty: '服务商未返回任何用量窗口。',
337
+ usageNoVisibleModels: '选择器中的可见模型没有任何用量窗口。',
329
338
  usageUnsupported: '该端点不提供用量查询。',
330
339
  usageError: '用量查询失败:{message}',
331
340
  usageSession: '5 小时窗口',
@@ -424,6 +433,9 @@ export const zh = {
424
433
  modelDefaultsSummaryEmpty: '没有模型声明推理档',
425
434
  modelDefaultsFilterPlaceholder: '筛选模型',
426
435
  modelDefaultsFilterEmpty: '没有匹配「{query}」的模型。',
436
+ modelDefaultsNoModels: '这个服务商没有报告任何模型。',
437
+ modelDefaultsNoAccount: '还没有连接账号。请先在卡片上连接一个账号。',
438
+ modelDefaultsDiscoveryFailed: '模型列表发现失败或超时。可以点「刷新」重试。',
427
439
  generating: '正在生成图片…',
428
440
  image: '图片',
429
441
  viewImage: '查看图片',
@@ -0,0 +1,37 @@
1
+ import type { SubagentSpeedTier } from '../provider-settings.js';
2
+ import type { AccountCatalogRow } from './account-preferences.js';
3
+ /** What the model editor shows for the current filter state. */
4
+ export type ModelListState = 'provider-empty' | 'filter-empty' | 'rows';
5
+ /** Distinguish an empty provider from a query that filtered out every row. */
6
+ export declare function modelListState(total: number, matched: number): ModelListState;
7
+ /**
8
+ * Retain saved model ids when discovery returns no rows. A temporary catalog
9
+ * failure must not hide the visibility or subagent-speed settings they edit.
10
+ */
11
+ export declare function savedModelIds(visibleModels: readonly string[] | undefined, subagentSpeeds: Readonly<Record<string, SubagentSpeedTier>> | undefined): string[];
12
+ /** Which notice replaces inert model controls. */
13
+ export type ModelEditorNotice = 'noAccount' | 'discoveryFailed' | 'noModels';
14
+ /**
15
+ * Use account discovery evidence to explain an empty provider. Connected
16
+ * accounts with failed discovery need a retry, not another login prompt.
17
+ */
18
+ export declare function modelEditorNotice(accounts: readonly AccountCatalogRow[] | undefined): ModelEditorNotice;
19
+ /** Everything the component needs to choose rows or one explanatory notice. */
20
+ export interface ModelEditorState {
21
+ listState: ModelListState;
22
+ /** Rendered only when `listState` is `provider-empty`. */
23
+ noticeKey: (typeof NOTICE_KEYS)[ModelEditorNotice];
24
+ }
25
+ /** Locale keys for the three empty states. */
26
+ export declare const NOTICE_KEYS: {
27
+ readonly noAccount: "modelDefaultsNoAccount";
28
+ readonly discoveryFailed: "modelDefaultsDiscoveryFailed";
29
+ readonly noModels: "modelDefaultsNoModels";
30
+ };
31
+ /** Combine discovered and saved rows, filter results, and discovery evidence. */
32
+ export declare function modelEditorState(input: {
33
+ discovered: number;
34
+ saved: number;
35
+ matched: number;
36
+ accounts: readonly AccountCatalogRow[] | undefined;
37
+ }): ModelEditorState;
@@ -0,0 +1,45 @@
1
+ /** Distinguish an empty provider from a query that filtered out every row. */
2
+ export function modelListState(total, matched) {
3
+ if (total === 0)
4
+ return 'provider-empty';
5
+ return matched === 0 ? 'filter-empty' : 'rows';
6
+ }
7
+ /**
8
+ * Retain saved model ids when discovery returns no rows. A temporary catalog
9
+ * failure must not hide the visibility or subagent-speed settings they edit.
10
+ */
11
+ export function savedModelIds(visibleModels, subagentSpeeds) {
12
+ return [
13
+ ...new Set([
14
+ ...(visibleModels ?? []),
15
+ ...Object.keys(subagentSpeeds ?? {}),
16
+ ]),
17
+ ];
18
+ }
19
+ /**
20
+ * Use account discovery evidence to explain an empty provider. Connected
21
+ * accounts with failed discovery need a retry, not another login prompt.
22
+ */
23
+ export function modelEditorNotice(accounts) {
24
+ // Account-neutral editors such as Cursor use the neutral empty-catalog copy.
25
+ if (accounts === undefined)
26
+ return 'noModels';
27
+ if (accounts.length === 0)
28
+ return 'noAccount';
29
+ return accounts.every((account) => account.unavailable === true)
30
+ ? 'discoveryFailed'
31
+ : 'noModels';
32
+ }
33
+ /** Locale keys for the three empty states. */
34
+ export const NOTICE_KEYS = {
35
+ noAccount: 'modelDefaultsNoAccount',
36
+ discoveryFailed: 'modelDefaultsDiscoveryFailed',
37
+ noModels: 'modelDefaultsNoModels',
38
+ };
39
+ /** Combine discovered and saved rows, filter results, and discovery evidence. */
40
+ export function modelEditorState(input) {
41
+ return {
42
+ listState: modelListState(input.discovered + input.saved, input.matched),
43
+ noticeKey: NOTICE_KEYS[modelEditorNotice(input.accounts)],
44
+ };
45
+ }
@@ -0,0 +1,7 @@
1
+ import type { UsageWindow } from './SubscriptionsSection.js';
2
+ /**
3
+ * Keep model-scoped Google quotas aligned with the saved model-picker selection.
4
+ * Account-wide windows and other providers are unaffected; an absent selection
5
+ * follows discovery, while an empty selection hides every model-scoped window.
6
+ */
7
+ export declare function visibleUsageWindows(provider: string, windows: UsageWindow[] | undefined, visibleModels: string[] | undefined): UsageWindow[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Keep model-scoped Google quotas aligned with the saved model-picker selection.
3
+ * Account-wide windows and other providers are unaffected; an absent selection
4
+ * follows discovery, while an empty selection hides every model-scoped window.
5
+ */
6
+ export function visibleUsageWindows(provider, windows, visibleModels) {
7
+ if (windows === undefined)
8
+ return [];
9
+ if (provider !== 'antigravity' || visibleModels === undefined)
10
+ return windows;
11
+ return windows.filter((window) => !window.scope || visibleModels.includes(window.scope));
12
+ }