dsh-subscriptions 0.1.5 → 0.2.2

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 (110) hide show
  1. package/README.md +212 -154
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.d.ts +73 -2
  6. package/lib/auth/rpc.js +124 -23
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +13 -0
  16. package/lib/client/OllamaCard.js +94 -0
  17. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  18. package/lib/client/OllamaSettingsManager.js +329 -0
  19. package/lib/client/ProviderAccountManager.d.ts +1 -1
  20. package/lib/client/ProviderAccountManager.js +164 -49
  21. package/lib/client/ProviderModelEditor.js +109 -26
  22. package/lib/client/SpeedSelect.d.ts +62 -14
  23. package/lib/client/SpeedSelect.js +257 -46
  24. package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
  25. package/lib/client/SubscriptionUsageBadge.js +301 -112
  26. package/lib/client/SubscriptionsSection.d.ts +2 -0
  27. package/lib/client/SubscriptionsSection.js +259 -85
  28. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  29. package/lib/client/UsageMeter.d.ts +1 -1
  30. package/lib/client/UsageMeter.js +69 -27
  31. package/lib/client/VideoGenerateToolview.js +69 -17
  32. package/lib/client/account-preferences.d.ts +8 -0
  33. package/lib/client/account-preferences.js +20 -5
  34. package/lib/client/host-contract.d.ts +11 -2
  35. package/lib/client/host-contract.js +20 -3
  36. package/lib/client/index.d.ts +5 -5
  37. package/lib/client/index.js +34 -12
  38. package/lib/client/locales.d.ts +78 -0
  39. package/lib/client/locales.js +121 -34
  40. package/lib/client/ollama-draft.d.ts +229 -0
  41. package/lib/client/ollama-draft.js +255 -0
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.d.ts +7 -0
  48. package/lib/client/usage-pace.js +43 -10
  49. package/lib/client.js +1647 -266
  50. package/lib/client.js.map +1 -1
  51. package/lib/http.js +1 -1
  52. package/lib/index.d.ts +28 -3
  53. package/lib/index.js +9329 -8217
  54. package/lib/model-defaults.js +4 -2
  55. package/lib/provider-settings.d.ts +14 -1
  56. package/lib/provider-settings.js +64 -20
  57. package/lib/providers/account-preferences.js +51 -22
  58. package/lib/providers/accounts.js +15 -9
  59. package/lib/providers/antigravity-oauth-client.js +2 -2
  60. package/lib/providers/antigravity.js +141 -68
  61. package/lib/providers/catalog-store.js +120 -41
  62. package/lib/providers/claude-cli-version.js +3 -3
  63. package/lib/providers/claude.d.ts +23 -3
  64. package/lib/providers/claude.js +167 -92
  65. package/lib/providers/codex-client-version.js +3 -3
  66. package/lib/providers/codex-search.js +40 -13
  67. package/lib/providers/codex.js +196 -112
  68. package/lib/providers/common.d.ts +14 -0
  69. package/lib/providers/common.js +118 -38
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.d.ts +9 -2
  77. package/lib/providers/external-usage-controller.js +30 -7
  78. package/lib/providers/external-usage.d.ts +8 -0
  79. package/lib/providers/external-usage.js +140 -20
  80. package/lib/providers/grok.js +152 -89
  81. package/lib/providers/image-pool.js +29 -11
  82. package/lib/providers/minimax-usage.js +31 -11
  83. package/lib/providers/npm-cli-version.js +10 -3
  84. package/lib/providers/ollama-settings.d.ts +90 -0
  85. package/lib/providers/ollama-settings.js +213 -0
  86. package/lib/providers/ollama-web.d.ts +46 -0
  87. package/lib/providers/ollama-web.js +231 -0
  88. package/lib/providers/ollama.d.ts +107 -0
  89. package/lib/providers/ollama.js +608 -0
  90. package/lib/providers/pool-family.js +11 -3
  91. package/lib/providers/pool-health.js +28 -8
  92. package/lib/providers/pool-usage.js +16 -9
  93. package/lib/providers/pool.d.ts +8 -0
  94. package/lib/providers/pool.js +81 -42
  95. package/lib/providers/rate-limit.js +8 -2
  96. package/lib/providers/replay.js +40 -17
  97. package/lib/providers/reset-redemption.js +18 -6
  98. package/lib/tools/image-generate.js +123 -65
  99. package/lib/tools/registration.js +5 -2
  100. package/lib/tools/video-generate.js +51 -31
  101. package/lib/tools/x-search.js +50 -22
  102. package/lib/translate/anthropic.js +85 -37
  103. package/lib/translate/antigravity-schema.js +71 -28
  104. package/lib/translate/antigravity-thinking.js +22 -9
  105. package/lib/translate/antigravity.js +141 -50
  106. package/lib/translate/chat-completions.js +67 -23
  107. package/lib/translate/resolved.js +55 -21
  108. package/lib/translate/responses.js +69 -29
  109. package/lib/translate/sse.js +4 -1
  110. package/package.json +26 -4
@@ -1,14 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useRef, useState } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
- import { accountModelRows, accountPoolSelection, mergeAccountChanges, mergeLatestAccounts } from './account-preferences.js';
4
+ import { accountModelRows, accountPoolSelection, mergeAccountChanges, mergeLatestAccounts, mergePromptCacheTtl, } from './account-preferences.js';
5
5
  import { ProviderModelEditor } from './ProviderModelEditor.js';
6
6
  import { providerSettingsCss } from './provider-settings-styles.js';
7
7
  const border = '0.5px solid var(--dsw-alias-border-l2)';
8
8
  const stack = { display: 'grid', gap: 12, minWidth: 0 };
9
- const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
9
+ const actions = {
10
+ display: 'flex',
11
+ flexWrap: 'wrap',
12
+ gap: 8,
13
+ alignItems: 'center',
14
+ };
10
15
  /** Native top-layer dialog provides keyboard containment, Escape and focus restoration. */
11
- export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
16
+ export function ProviderAccountManager({ provider, name, rpc, t, onClose, }) {
12
17
  const dialog = useRef(null);
13
18
  const title = useId();
14
19
  const description = useId();
@@ -20,6 +25,9 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
20
25
  const [saveError, setSaveError] = useState('');
21
26
  const [attempt, setAttempt] = useState(0);
22
27
  const [modelsDirty, setModelsDirty] = useState(false);
28
+ // The Claude prompt-cache TTL draft: undefined follows the plugin config.
29
+ const [ttlDraft, setTtlDraft] = useState();
30
+ const [ttlDirty, setTtlDirty] = useState(false);
23
31
  const alive = useRef(true);
24
32
  const saveLock = useRef(false);
25
33
  const editor = useRef(null);
@@ -39,18 +47,34 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
39
47
  let current = true;
40
48
  setLoading(true);
41
49
  setError('');
42
- void callSubscriptionsAuth(rpc, provider === 'cursor-subscription' ? 'cursorSettings' : 'providerSettings', { provider }).then(data => {
43
- if (current)
50
+ void callSubscriptionsAuth(rpc, provider === 'cursor-subscription'
51
+ ? 'cursorSettings'
52
+ : 'providerSettings', { provider })
53
+ .then((data) => {
54
+ if (current) {
44
55
  setCatalog(data);
45
- }).catch(error => {
56
+ setTtlDraft(data.settings.promptCacheTtl);
57
+ setTtlDirty(false);
58
+ }
59
+ })
60
+ .catch((error) => {
61
+ if (current)
62
+ setError(t('accountsLoadFailed', {
63
+ message: error instanceof Error ? error.message : String(error),
64
+ }));
65
+ })
66
+ .finally(() => {
46
67
  if (current)
47
- setError(t('accountsLoadFailed', { message: error instanceof Error ? error.message : String(error) }));
48
- }).finally(() => { if (current)
49
- setLoading(false); });
50
- return () => { current = false; };
68
+ setLoading(false);
69
+ });
70
+ return () => {
71
+ current = false;
72
+ };
73
+ // `t` only formats the load error. A new translator must not refetch.
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- refetch on provider, rpc, and attempt only
51
75
  }, [provider, rpc, attempt]);
52
76
  function edit(key, next) {
53
- setChanges(current => ({ ...current, [key]: next }));
77
+ setChanges((current) => ({ ...current, [key]: next }));
54
78
  }
55
79
  /**
56
80
  * One submit for both drafts: the model editor's (visibility, effort,
@@ -72,7 +96,11 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
72
96
  try {
73
97
  if (provider !== 'cursor-subscription') {
74
98
  for (const { model, effort } of models.efforts) {
75
- await callSubscriptionsAuth(rpc, 'setModelDefault', { provider, model, ...(effort ? { effort } : {}) });
99
+ await callSubscriptionsAuth(rpc, 'setModelDefault', {
100
+ provider,
101
+ model,
102
+ ...(effort ? { effort } : {}),
103
+ });
76
104
  savedEfforts++;
77
105
  if (!alive.current)
78
106
  return;
@@ -80,15 +108,28 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
80
108
  }
81
109
  // Re-read before writing: another client may have saved since this
82
110
  // dialog loaded, and a stale snapshot must never be written back.
83
- const latest = await callSubscriptionsAuth(rpc, provider === 'cursor-subscription' ? 'cursorSettings' : 'providerSettings', { provider });
111
+ const latest = await callSubscriptionsAuth(rpc, provider === 'cursor-subscription'
112
+ ? 'cursorSettings'
113
+ : 'providerSettings', { provider });
84
114
  if (!alive.current)
85
115
  return;
86
116
  if (provider === 'cursor-subscription') {
87
- await callSubscriptionsAuth(rpc, 'cursorSetSettings', { settings: models.settings ?? latest.settings });
117
+ await callSubscriptionsAuth(rpc, 'cursorSetSettings', {
118
+ settings: models.settings ?? latest.settings,
119
+ });
88
120
  }
89
121
  else {
90
- const base = models.settings === undefined ? latest.settings : mergeLatestAccounts(models.settings, latest.settings);
91
- await callSubscriptionsAuth(rpc, 'setProviderSettings', { provider, settings: mergeAccountChanges(base, changes) });
122
+ const base = models.settings === undefined
123
+ ? latest.settings
124
+ : mergeLatestAccounts(models.settings, latest.settings);
125
+ // The editor's draft carries the TTL it loaded, so the dialog always
126
+ // replaces it: the picker's choice when touched, otherwise the value
127
+ // from the freshly re-read settings.
128
+ const withTtl = mergePromptCacheTtl(base, ttlDirty ? ttlDraft : latest.settings.promptCacheTtl);
129
+ await callSubscriptionsAuth(rpc, 'setProviderSettings', {
130
+ provider,
131
+ settings: mergeAccountChanges(withTtl, changes),
132
+ });
92
133
  }
93
134
  if (alive.current)
94
135
  onClose();
@@ -96,7 +137,8 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
96
137
  catch (error) {
97
138
  if (alive.current) {
98
139
  const message = error instanceof Error ? error.message : String(error);
99
- setSaveError((savedEfforts ? t('modelsPartialSave') + ' ' : '') + t('accountsSaveFailed', { message }));
140
+ setSaveError((savedEfforts ? t('modelsPartialSave') + ' ' : '') +
141
+ t('accountsSaveFailed', { message }));
100
142
  }
101
143
  }
102
144
  finally {
@@ -105,36 +147,109 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
105
147
  setSaving(false);
106
148
  }
107
149
  }
108
- return _jsxs("dialog", { className: "dsh-subscription-manager", ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: event => { if (saveLock.current)
109
- event.preventDefault(); }, onClose: onClose, style: { width: 620, maxWidth: 'calc(100vw - 32px)', maxHeight: 'calc(100dvh - 32px)', overflow: 'auto' }, children: [_jsx("style", { children: providerSettingsCss }), _jsxs("div", { style: { ...stack, padding: 20 }, children: [_jsxs("header", { style: { ...actions, justifyContent: 'space-between' }, children: [_jsx("h2", { id: title, style: { margin: 0 }, children: t('accountsTitle', { provider: name }) }), _jsx("button", { type: "button", autoFocus: true, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription' ? 'cursorAccountsHint' : 'accountsHint') }), error && _jsx("p", { role: "alert", children: error }), loading && _jsx("p", { role: "status", className: "dsh-subscription-hint", children: t('accountsLoading') }), !loading && !catalog && _jsx("button", { type: "button", onClick: () => setAttempt(value => value + 1), children: t('modelDefaultsRetry') }), catalog && _jsxs("fieldset", { disabled: saving || loading, style: { ...stack, border: 0, margin: 0, padding: 0 }, children: [catalog.accounts.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription' ? 'cursorManageSignIn' : 'accountsEmpty') }), provider === 'cursor-subscription' && catalog.accounts.length > 0 &&
110
- _jsxs("fieldset", { style: { border, borderRadius: 'var(--dsw-radius-lg, 12px)', padding: 14 }, children: [_jsx("legend", { style: { padding: '0 6px' }, children: name }), _jsx("p", { className: "dsh-subscription-hint", children: t('cursorConnected') })] }), provider !== 'cursor-subscription' && catalog.accounts.map(account => {
111
- const preferences = changes[account.key] ?? catalog.settings.accounts?.[account.key] ?? {};
112
- const selected = accountPoolSelection(preferences, account.models);
113
- const models = accountModelRows(account, preferences);
114
- return _jsxs("fieldset", { style: { ...stack, border, borderRadius: 'var(--dsw-radius-lg, 12px)', padding: 14, margin: 0 }, children: [_jsx("legend", { style: { padding: '0 6px', overflowWrap: 'anywhere', maxWidth: '100%' }, children: account.label }), account.unavailable && _jsx("p", { className: "dsh-subscription-hint", children: t('accountsCatalogUnavailable') }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('accountsAlias') }), _jsx("input", { value: preferences.alias ?? '', placeholder: account.label, onChange: event => {
115
- const next = { ...preferences };
116
- if (event.target.value.trim())
117
- next.alias = event.target.value;
118
- else
119
- delete next.alias;
120
- edit(account.key, next);
121
- } })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolEnabled !== false, onChange: event => edit(account.key, { ...preferences, poolEnabled: event.target.checked }) }), " ", t('accountsPoolEnabled')] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.independentEntry === true, onChange: event => edit(account.key, { ...preferences, independentEntry: event.target.checked }) }), " ", t('accountsIndependent')] }), _jsx("p", { className: "dsh-subscription-hint", children: t('accountsIndependentHint') }), _jsxs("details", { children: [_jsxs("summary", { style: { cursor: 'pointer', fontSize: 13 }, children: [t('accountsModels'), " \u00B7 ", preferences.poolModels === undefined
122
- ? t('accountsModelsAll') : t('accountsModelsCount', { count: preferences.poolModels.length })] }), _jsxs("div", { style: { ...stack, marginTop: 12 }, children: [_jsx("p", { className: "dsh-subscription-hint", children: t('accountsModelsHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolModels === undefined, onChange: event => {
123
- const next = { ...preferences };
124
- if (event.target.checked)
125
- delete next.poolModels;
126
- else
127
- next.poolModels = account.models.map(model => model.id);
128
- edit(account.key, next);
129
- } }), " ", t('accountsModelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", onClick: () => edit(account.key, { ...preferences, poolModels: models.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit(account.key, { ...preferences, poolModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { ...stack, gap: 8, maxHeight: 240, overflowY: 'auto' }, children: [models.map(model => _jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
130
- const next = new Set(selected);
131
- if (event.target.checked)
132
- next.add(model.id);
133
- else
134
- next.delete(model.id);
135
- edit(account.key, { ...preferences, poolModels: [...next] });
136
- } }), " ", model.name, model.unavailable && ` (${t('modelsUnavailable')})`] }, model.id)), models.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t('accountsNoModels') })] })] })] })] }, account.key);
137
- })] }), (provider !== 'cursor-subscription' || (catalog?.accounts.length ?? 0) > 0) &&
138
- _jsx(ProviderModelEditor, { ref: editor, provider: provider, rpc: rpc, t: t, disabled: saving, onDirtyChange: setModelsDirty }), _jsxs("footer", { style: { ...stack, gap: 8, borderTop: border, padding: '14px 0 8px',
139
- position: 'sticky', bottom: 0, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))' }, children: [saveError && _jsx("p", { role: "alert", children: saveError }), _jsxs("div", { style: { ...actions, justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: loading || saving || (!Object.keys(changes).length && !modelsDirty), onClick: () => { void save(); }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] })] });
150
+ return (_jsxs("dialog", { className: "dsh-subscription-manager", ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: (event) => {
151
+ if (saveLock.current)
152
+ event.preventDefault();
153
+ }, onClose: onClose, style: {
154
+ width: 620,
155
+ maxWidth: 'calc(100vw - 32px)',
156
+ maxHeight: 'calc(100dvh - 32px)',
157
+ overflow: 'auto',
158
+ }, children: [_jsx("style", { children: providerSettingsCss }), _jsxs("div", { style: { ...stack, padding: 20 }, children: [_jsxs("header", { style: { ...actions, justifyContent: 'space-between' }, children: [_jsx("h2", { id: title, style: { margin: 0 }, children: t('accountsTitle', { provider: name }) }), _jsx("button", { type: "button", autoFocus: true, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription'
159
+ ? 'cursorAccountsHint'
160
+ : 'accountsHint') }), error && _jsx("p", { role: "alert", children: error }), loading && (_jsx("p", { role: "status", className: "dsh-subscription-hint", children: t('accountsLoading') })), !loading && !catalog && (_jsx("button", { type: "button", onClick: () => setAttempt((value) => value + 1), children: t('modelDefaultsRetry') })), catalog && (_jsxs("fieldset", { disabled: saving || loading, style: { ...stack, border: 0, margin: 0, padding: 0 }, children: [catalog.accounts.length === 0 && (_jsx("p", { className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription'
161
+ ? 'cursorManageSignIn'
162
+ : 'accountsEmpty') })), provider === 'cursor-subscription' &&
163
+ catalog.accounts.length > 0 && (_jsxs("fieldset", { style: {
164
+ border,
165
+ borderRadius: 'var(--dsw-radius-lg, 12px)',
166
+ padding: 14,
167
+ }, children: [_jsx("legend", { style: { padding: '0 6px' }, children: name }), _jsx("p", { className: "dsh-subscription-hint", children: t('cursorConnected') })] })), provider !== 'cursor-subscription' &&
168
+ catalog.accounts.map((account) => {
169
+ const preferences = changes[account.key] ??
170
+ catalog.settings.accounts?.[account.key] ??
171
+ {};
172
+ const selected = accountPoolSelection(preferences, account.models);
173
+ const models = accountModelRows(account, preferences);
174
+ return (_jsxs("fieldset", { style: {
175
+ ...stack,
176
+ border,
177
+ borderRadius: 'var(--dsw-radius-lg, 12px)',
178
+ padding: 14,
179
+ margin: 0,
180
+ }, children: [_jsx("legend", { style: {
181
+ padding: '0 6px',
182
+ overflowWrap: 'anywhere',
183
+ maxWidth: '100%',
184
+ }, children: account.label }), account.unavailable && (_jsx("p", { className: "dsh-subscription-hint", children: t('accountsCatalogUnavailable') })), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('accountsAlias') }), _jsx("input", { value: preferences.alias ?? '', placeholder: account.label, onChange: (event) => {
185
+ const next = { ...preferences };
186
+ if (event.target.value.trim())
187
+ next.alias = event.target.value;
188
+ else
189
+ delete next.alias;
190
+ edit(account.key, next);
191
+ } })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolEnabled !== false, onChange: (event) => edit(account.key, {
192
+ ...preferences,
193
+ poolEnabled: event.target.checked,
194
+ }) }), ' ', t('accountsPoolEnabled')] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.independentEntry === true, onChange: (event) => edit(account.key, {
195
+ ...preferences,
196
+ independentEntry: event.target.checked,
197
+ }) }), ' ', t('accountsIndependent')] }), _jsx("p", { className: "dsh-subscription-hint", children: t('accountsIndependentHint') }), _jsxs("details", { children: [_jsxs("summary", { style: { cursor: 'pointer', fontSize: 13 }, children: [t('accountsModels'), " \u00B7", ' ', preferences.poolModels === undefined
198
+ ? t('accountsModelsAll')
199
+ : t('accountsModelsCount', {
200
+ count: preferences.poolModels.length,
201
+ })] }), _jsxs("div", { style: { ...stack, marginTop: 12 }, children: [_jsx("p", { className: "dsh-subscription-hint", children: t('accountsModelsHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolModels === undefined, onChange: (event) => {
202
+ const next = { ...preferences };
203
+ if (event.target.checked)
204
+ delete next.poolModels;
205
+ else
206
+ next.poolModels = account.models.map((model) => model.id);
207
+ edit(account.key, next);
208
+ } }), ' ', t('accountsModelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", onClick: () => edit(account.key, {
209
+ ...preferences,
210
+ poolModels: models.map((model) => model.id),
211
+ }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit(account.key, {
212
+ ...preferences,
213
+ poolModels: [],
214
+ }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: {
215
+ ...stack,
216
+ gap: 8,
217
+ maxHeight: 240,
218
+ overflowY: 'auto',
219
+ }, children: [models.map((model) => (_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: (event) => {
220
+ const next = new Set(selected);
221
+ if (event.target.checked)
222
+ next.add(model.id);
223
+ else
224
+ next.delete(model.id);
225
+ edit(account.key, {
226
+ ...preferences,
227
+ poolModels: [...next],
228
+ });
229
+ } }), ' ', model.name, model.unavailable &&
230
+ ` (${t('modelsUnavailable')})`] }, model.id))), models.length === 0 && (_jsx("p", { className: "dsh-subscription-hint", children: t('accountsNoModels') }))] })] })] })] }, account.key));
231
+ })] })), (provider !== 'cursor-subscription' ||
232
+ (catalog?.accounts.length ?? 0) > 0) && (_jsx(ProviderModelEditor, { ref: editor, provider: provider, rpc: rpc, t: t, disabled: saving, onDirtyChange: setModelsDirty })), provider === 'claude' && catalog && (_jsxs("div", { style: {
233
+ ...stack,
234
+ gap: 6,
235
+ borderTop: border,
236
+ marginTop: 12,
237
+ paddingTop: 12,
238
+ }, children: [_jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('claudeCacheTtl') }), _jsxs("select", { "aria-label": t('claudeCacheTtl'), style: { width: 220 }, value: ttlDraft ?? '', disabled: saving, onChange: (event) => {
239
+ const value = event.target.value;
240
+ setTtlDraft(value === '' ? undefined : value);
241
+ setTtlDirty(true);
242
+ }, children: [_jsx("option", { value: "", children: t('claudeCacheTtlFollowConfig') }), _jsx("option", { value: "5m", children: t('claudeCacheTtl5m') }), _jsx("option", { value: "1h", children: t('claudeCacheTtl1h') })] })] }), _jsx("p", { className: "dsh-subscription-hint", children: t('claudeCacheTtlHint') })] })), _jsxs("footer", { style: {
243
+ ...stack,
244
+ gap: 8,
245
+ borderTop: border,
246
+ padding: '14px 0 8px',
247
+ position: 'sticky',
248
+ bottom: 0,
249
+ background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))',
250
+ }, children: [saveError && _jsx("p", { role: "alert", children: saveError }), _jsxs("div", { style: { ...actions, justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: loading ||
251
+ saving ||
252
+ (!Object.keys(changes).length && !modelsDirty && !ttlDirty), onClick: () => {
253
+ void save();
254
+ }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] })] }));
140
255
  }
@@ -1,8 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
+ /** Providers whose runtime honours a local context-window override. */
5
+ const supportsContext = (provider) => provider === 'codex' || provider === 'claude';
4
6
  const border = '0.5px solid var(--dsw-alias-border-l2, #ddd)';
5
- const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
7
+ const actions = {
8
+ display: 'flex',
9
+ flexWrap: 'wrap',
10
+ gap: 8,
11
+ alignItems: 'center',
12
+ };
6
13
  /**
7
14
  * Embedded section of the account manager dialog. It owns the model draft
8
15
  * (visibility, effort, context, tools) but not its submission: the dialog
@@ -19,12 +26,19 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
19
26
  const [dirty, setDirty] = useState(false);
20
27
  const [error, setError] = useState('');
21
28
  const generation = useRef(0);
22
- useEffect(() => () => { generation.current++; }, []);
23
- useEffect(() => { onDirtyChange?.(dirty); }, [dirty]);
29
+ useEffect(() => () => {
30
+ generation.current++;
31
+ }, []);
32
+ // Notify only when dirty changes. The parent passes a fresh callback each render.
33
+ /* eslint-disable react-hooks/exhaustive-deps -- onDirtyChange is not a trigger */
34
+ useEffect(() => {
35
+ onDirtyChange?.(dirty);
36
+ }, [dirty]);
37
+ /* eslint-enable react-hooks/exhaustive-deps */
24
38
  function reset(data) {
25
39
  setCatalog(data);
26
40
  setDraft(data.settings);
27
- setEfforts(Object.fromEntries(data.models.map(model => [model.id, model.configured ?? ''])));
41
+ setEfforts(Object.fromEntries(data.models.map((model) => [model.id, model.configured ?? ''])));
28
42
  setContexts(Object.fromEntries(Object.entries(data.settings.contextWindows ?? {}).map(([id, value]) => [id, String(value)])));
29
43
  setDirty(false);
30
44
  }
@@ -33,7 +47,9 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
33
47
  setBusy(true);
34
48
  setError('');
35
49
  try {
36
- const data = await callSubscriptionsAuth(rpc, provider === 'cursor-subscription' ? 'cursorSettings' : 'providerSettings', { provider, force });
50
+ const data = await callSubscriptionsAuth(rpc, provider === 'cursor-subscription'
51
+ ? 'cursorSettings'
52
+ : 'providerSettings', { provider, force });
37
53
  if (generation.current === request)
38
54
  reset(data);
39
55
  }
@@ -46,8 +62,16 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
46
62
  setBusy(false);
47
63
  }
48
64
  }
49
- function edit(next) { setDraft(next); setDirty(true); }
50
- useEffect(() => { void load(); }, [provider, rpc]);
65
+ function edit(next) {
66
+ setDraft(next);
67
+ setDirty(true);
68
+ }
69
+ // `load` is a new function each render; provider and rpc are the triggers.
70
+ /* eslint-disable react-hooks/exhaustive-deps -- avoid refetching every render */
71
+ useEffect(() => {
72
+ void load();
73
+ }, [provider, rpc]);
74
+ /* eslint-enable react-hooks/exhaustive-deps */
51
75
  useImperativeHandle(ref, () => ({
52
76
  collect() {
53
77
  if (!catalog || !dirty)
@@ -57,47 +81,106 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
57
81
  if (!text.trim())
58
82
  continue;
59
83
  const value = Number(text);
60
- if (!/^\d+$/.test(text.trim()) || !Number.isSafeInteger(value) || value <= 0) {
84
+ if (!/^\d+$/.test(text.trim()) ||
85
+ !Number.isSafeInteger(value) ||
86
+ value <= 0) {
61
87
  setError(t('modelsContextInvalid', { model }));
62
88
  return undefined;
63
89
  }
64
90
  windows[model] = value;
65
91
  }
66
92
  setError('');
67
- const { accounts: _accounts, ...settings } = { ...draft, ...(provider === 'codex' ? { contextWindows: windows } : {}) };
93
+ const { accounts: _accounts, ...settings } = {
94
+ ...draft,
95
+ ...(supportsContext(provider) ? { contextWindows: windows } : {}),
96
+ };
68
97
  return {
69
98
  settings,
70
- efforts: catalog.models.flatMap(model => {
99
+ efforts: catalog.models.flatMap((model) => {
71
100
  const effort = efforts[model.id] ?? '';
72
- return effort === (model.configured ?? '') || !model.efforts?.length ? [] : [{ model: model.id, effort }];
101
+ return effort === (model.configured ?? '') ||
102
+ !model.efforts?.length
103
+ ? []
104
+ : [{ model: model.id, effort }];
73
105
  }),
74
106
  };
75
107
  },
76
- }), [catalog, dirty, draft, contexts, efforts, provider]);
108
+ }), [catalog, dirty, draft, contexts, efforts, provider, t]);
77
109
  const allModels = catalog?.models ?? [];
78
- const known = new Set(allModels.map(model => model.id));
79
- const missing = (draft.visibleModels ?? []).filter(id => !known.has(id)).map(id => ({ id, name: id }));
80
- const models = [...allModels, ...missing].filter(model => `${model.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()));
81
- const selected = new Set(draft.visibleModels ?? allModels.map(model => model.id));
82
- 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' ? 'cursorManageHint' : '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: () => { void load(true); }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), catalog && _jsxs("fieldset", { disabled: busy || disabled, style: { border: 0, padding: 0, margin: 0, minWidth: 0, display: 'grid', gap: 12 }, children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.visibleModels === undefined, onChange: event => {
110
+ const known = new Set(allModels.map((model) => model.id));
111
+ const missing = (draft.visibleModels ?? [])
112
+ .filter((id) => !known.has(id))
113
+ .map((id) => ({ id, name: id }));
114
+ const models = [...allModels, ...missing].filter((model) => `${model.name} ${model.id}`
115
+ .toLowerCase()
116
+ .includes(query.trim().toLowerCase()));
117
+ const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
118
+ 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
+ ? 'cursorManageHint'
120
+ : '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
+ void load(true);
122
+ }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), catalog && (_jsxs("fieldset", { disabled: busy || disabled, style: {
123
+ border: 0,
124
+ padding: 0,
125
+ margin: 0,
126
+ minWidth: 0,
127
+ display: 'grid',
128
+ gap: 12,
129
+ }, children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.visibleModels === undefined, onChange: (event) => {
83
130
  const next = { ...draft };
84
131
  if (event.target.checked)
85
132
  delete next.visibleModels;
86
133
  else
87
- next.visibleModels = allModels.map(model => model.id);
134
+ next.visibleModels = allModels.map((model) => model.id);
88
135
  edit(next);
89
- } }), " ", 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({ ...draft, visibleModels: allModels.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit({ ...draft, visibleModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { maxHeight: 360, overflowY: 'auto', display: 'grid', gap: 8 }, children: [models.map(model => _jsxs("div", { style: { borderBottom: border, padding: '8px 2px', display: 'grid', gap: 8 }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
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',
147
+ display: 'grid',
148
+ gap: 8,
149
+ }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: (event) => {
90
150
  const next = new Set(selected);
91
151
  if (event.target.checked)
92
152
  next.add(model.id);
93
153
  else
94
154
  next.delete(model.id);
95
155
  edit({ ...draft, visibleModels: [...next] });
96
- } }), " ", 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 => {
97
- setEfforts({ ...efforts, [model.id]: event.target.value });
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
+ });
98
161
  setDirty(true);
99
- }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured && !model.efforts?.some(effort => effort.id === model.configured) &&
100
- _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))] })] }), provider === 'codex' && 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 => { setContexts({ ...contexts, [model.id]: event.target.value }); setDirty(true); } })] }), _jsx("small", { children: t('modelsContextBounds', { default: model.defaultContextWindow, max: model.maxContextWindow }) })] })] })] }, model.id)), models.length === 0 && _jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) })] }), provider === 'codex' && _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({
101
- ...draft, tools: { ...draft.tools, [tool]: event.target.checked },
102
- }) }), " ", t(tool === 'image_generate' ? 'modelsImage' : tool === 'video_generate' ? 'modelsVideo' : tool === 'web_search' ? 'modelsWebSearch' : 'modelsSearch')] }, tool))] })] })] })] });
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,
167
+ [model.id]: event.target.value,
168
+ });
169
+ 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({
174
+ ...draft,
175
+ tools: {
176
+ ...draft.tools,
177
+ [tool]: event.target.checked,
178
+ },
179
+ }) }), ' ', t(tool === 'image_generate'
180
+ ? 'modelsImage'
181
+ : tool === 'video_generate'
182
+ ? 'modelsVideo'
183
+ : tool === 'web_search'
184
+ ? 'modelsWebSearch'
185
+ : 'modelsSearch')] }, tool)))] }))] }))] })] }));
103
186
  });
@@ -19,16 +19,28 @@ export interface SpeedSelectState {
19
19
  * ship it with this shape; it replaces rc.2's `connection.api.sessions.models`
20
20
  * read, which the 0.1.2-alpha removed along with the whole `.api` face.
21
21
  */
22
+ /** Selected model pushed by ui-model-selection's directory store. */
23
+ export interface ModelSelectionSnapshot {
24
+ current: {
25
+ provider: string;
26
+ model: string;
27
+ } | null;
28
+ }
29
+ /**
30
+ * The directory store (`ModelDirectory.store`). Present on hosts that publish
31
+ * selection changes; absent on a directory that only offers `load()`.
32
+ */
33
+ export interface ModelSelectionStore {
34
+ subscribe(listener: () => void): () => void;
35
+ getSnapshot(): ModelSelectionSnapshot;
36
+ }
22
37
  export interface ModelDirectoriesLike {
23
38
  /** Resolve one session's shared model directory (throws for unknown sessions). */
24
39
  directoryFor(sessionId: string): {
25
40
  /** Load the directory; `current` is the session's effective model selection. */
26
- load(): Promise<{
27
- current: {
28
- provider: string;
29
- model: string;
30
- } | null;
31
- }>;
41
+ load(): Promise<ModelSelectionSnapshot>;
42
+ /** Pushes selection changes. Missing when the host only offers `load()`. */
43
+ store?: ModelSelectionStore;
32
44
  };
33
45
  }
34
46
  /** Injected dependencies of {@link SpeedSelect} (slot `inject`, session-bound). */
@@ -37,6 +49,12 @@ export interface SpeedSelectInjected {
37
49
  loadSpeed: () => Promise<SpeedSelectState>;
38
50
  /** Set the session's speed tier; resolves false when the write failed. */
39
51
  setSpeed: (tier: SpeedTier) => Promise<boolean>;
52
+ /**
53
+ * Subscribe to the session's selected model. The listener runs when
54
+ * `provider` or `model` changes, and once when a directory that was missing
55
+ * at subscribe time becomes available. The returned function detaches it.
56
+ */
57
+ watchModel: (listener: () => void) => () => void;
40
58
  }
41
59
  /**
42
60
  * Props delivered by the slot outlet: the framework session kit and InputZone
@@ -46,9 +64,10 @@ export interface SpeedSelectInjected {
46
64
  export type SpeedSelectProps = PropsRuntime<'conversation.input.right'> & Partial<SpeedSelectInjected> & Partial<PropsLocale<'settings.subscriptions'>>;
47
65
  /**
48
66
  * The `loadSpeed` half of the inject face: the plugin's own speed state plus
49
- * the host's current model selection (the visibility gate). A model-RPC
50
- * failure throws rather than answering "hidden" — the caller keeps its last
51
- * known state, so a transient failure never locks the toggle away.
67
+ * the host's current model selection (the visibility gate). A session whose
68
+ * model selection is unavailable (an addressed subagent) answers hidden.
69
+ * Any other model-directory failure throws, so the caller keeps its last
70
+ * known state instead of locking the toggle away.
52
71
  *
53
72
  * `sessionId` is a plain string: slot and command contexts brand it through
54
73
  * different dsh-session copies, and only the service boundary needs one.
@@ -58,14 +77,43 @@ export type SpeedSelectProps = PropsRuntime<'conversation.input.right'> & Partia
58
77
  * at all) simply keeps the toggle hidden.
59
78
  */
60
79
  export declare function createSpeedLoader(connection: ConnectionHandle, models: () => ModelDirectoriesLike | undefined, sessionId: string): SpeedSelectInjected['loadSpeed'];
80
+ /** Stable identity of a model selection. Empty when nothing is selected. */
81
+ export declare function modelSelectionKey(current: ModelSelectionSnapshot['current'] | undefined): string;
82
+ /**
83
+ * How often a host without a directory store is re-read, and how often a
84
+ * missing directory service is retried. Neither interval calls
85
+ * `subscriptions-auth.speed`.
86
+ */
87
+ export declare const MODEL_DIRECTORY_POLL_INTERVAL_MS = 3000;
88
+ /**
89
+ * Mount reads of `speed`, including the first attempt. Further failures stop
90
+ * instead of polling for the life of the composer.
91
+ */
92
+ export declare const SPEED_MOUNT_READ_ATTEMPTS = 4;
93
+ /** Fired after `/fast` writes a tier, so an open composer re-reads `speed`. */
94
+ export declare const SPEED_CHANGED_EVENT = "dsh:subscriptions:speed-changed";
95
+ /** Tell mounted Speed controls that this page changed a session tier. */
96
+ export declare function notifySpeedChanged(): void;
97
+ /**
98
+ * Subscribe to the session's selected model. A directory store notifies the
99
+ * listener only when `provider` or `model` changes. A directory with no store
100
+ * is re-read on {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}; a service that is
101
+ * not registered yet, or whose `directoryFor` throws, is retried on that same
102
+ * interval. The first successful attach after a miss notifies once, so a
103
+ * mount that ran before the service existed reads `speed` again.
104
+ */
105
+ export declare function createModelSelectionWatch(models: () => ModelDirectoriesLike | undefined, sessionId: string): SpeedSelectInjected['watchModel'];
61
106
  /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
62
107
  export declare function createSpeedSetter(connection: ConnectionHandle, sessionId: string): SpeedSelectInjected['setSpeed'];
63
108
  /**
64
109
  * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
65
110
  * that opens a two-row menu (standard/fast with descriptions, check mark on
66
- * the current tier). The host pushes nothing on a model switch, so the
67
- * control re-reads on a slow poll with a single-flight guard; a failed read
68
- * keeps the last known state, so a transient RPC failure can never lock the
69
- * toggle away (the earlier mount-only load had no recovery path).
111
+ * the current tier). It reads `subscriptions-auth.speed` on mount, when the
112
+ * menu opens, when {@link SpeedSelectInjected.watchModel} reports a new
113
+ * selected model, and when {@link notifySpeedChanged} fires. Until the first
114
+ * read succeeds, a failure is retried on
115
+ * {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}, at most
116
+ * {@link SPEED_MOUNT_READ_ATTEMPTS} times; after that, a failed read keeps
117
+ * the last known state.
70
118
  */
71
- export declare function SpeedSelect({ loadSpeed, setSpeed, t }: SpeedSelectProps): import("react").JSX.Element | null;
119
+ export declare function SpeedSelect({ loadSpeed, setSpeed, watchModel, t, }: SpeedSelectProps): import("react").JSX.Element | null;