dsh-subscriptions 0.2.0 → 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 (99) hide show
  1. package/README.md +212 -180
  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.js +74 -23
  6. package/lib/auth/store.js +80 -31
  7. package/lib/client/CursorCard.d.ts +1 -1
  8. package/lib/client/CursorCard.js +50 -16
  9. package/lib/client/ExternalUsageCards.d.ts +1 -1
  10. package/lib/client/ExternalUsageCards.js +37 -16
  11. package/lib/client/ImageGallery.d.ts +2 -2
  12. package/lib/client/ImageGallery.js +92 -33
  13. package/lib/client/ImageGenerateToolview.js +50 -14
  14. package/lib/client/OllamaCard.d.ts +1 -1
  15. package/lib/client/OllamaCard.js +26 -8
  16. package/lib/client/OllamaSettingsManager.js +113 -30
  17. package/lib/client/ProviderAccountManager.d.ts +1 -1
  18. package/lib/client/ProviderAccountManager.js +164 -49
  19. package/lib/client/ProviderModelEditor.js +107 -26
  20. package/lib/client/SpeedSelect.d.ts +62 -14
  21. package/lib/client/SpeedSelect.js +257 -46
  22. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  23. package/lib/client/SubscriptionUsageBadge.js +300 -112
  24. package/lib/client/SubscriptionsSection.js +258 -85
  25. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  26. package/lib/client/UsageMeter.d.ts +1 -1
  27. package/lib/client/UsageMeter.js +62 -24
  28. package/lib/client/VideoGenerateToolview.js +69 -17
  29. package/lib/client/account-preferences.d.ts +8 -0
  30. package/lib/client/account-preferences.js +20 -5
  31. package/lib/client/host-contract.d.ts +11 -2
  32. package/lib/client/host-contract.js +20 -3
  33. package/lib/client/index.d.ts +5 -5
  34. package/lib/client/index.js +34 -12
  35. package/lib/client/locales.d.ts +10 -0
  36. package/lib/client/locales.js +61 -51
  37. package/lib/client/ollama-draft.js +21 -12
  38. package/lib/client/reset-action.js +6 -4
  39. package/lib/client/reset-credits-view.d.ts +1 -1
  40. package/lib/client/reset-credits-view.js +66 -17
  41. package/lib/client/subscription-card-styles.js +94 -24
  42. package/lib/client/usage-color-preferences.js +4 -2
  43. package/lib/client/usage-pace.js +30 -12
  44. package/lib/client.js +287 -47
  45. package/lib/client.js.map +1 -1
  46. package/lib/http.js +1 -1
  47. package/lib/index.d.ts +3 -3
  48. package/lib/index.js +118 -64
  49. package/lib/model-defaults.js +4 -2
  50. package/lib/provider-settings.d.ts +12 -0
  51. package/lib/provider-settings.js +58 -16
  52. package/lib/providers/account-preferences.js +51 -22
  53. package/lib/providers/accounts.js +15 -9
  54. package/lib/providers/antigravity-oauth-client.js +2 -2
  55. package/lib/providers/antigravity.js +141 -68
  56. package/lib/providers/catalog-store.js +120 -41
  57. package/lib/providers/claude-cli-version.js +3 -3
  58. package/lib/providers/claude.d.ts +9 -2
  59. package/lib/providers/claude.js +153 -90
  60. package/lib/providers/codex-client-version.js +3 -3
  61. package/lib/providers/codex-search.js +40 -13
  62. package/lib/providers/codex.js +196 -112
  63. package/lib/providers/common.js +71 -39
  64. package/lib/providers/copilot.js +143 -82
  65. package/lib/providers/cursor-adapter.js +42 -18
  66. package/lib/providers/cursor-auth.d.ts +1 -1
  67. package/lib/providers/cursor-auth.js +69 -24
  68. package/lib/providers/cursor-model-settings.js +14 -5
  69. package/lib/providers/cursor-usage.js +27 -13
  70. package/lib/providers/external-usage-controller.js +14 -5
  71. package/lib/providers/external-usage.js +59 -23
  72. package/lib/providers/grok.js +152 -89
  73. package/lib/providers/image-pool.js +29 -11
  74. package/lib/providers/minimax-usage.js +31 -11
  75. package/lib/providers/npm-cli-version.js +10 -3
  76. package/lib/providers/ollama-settings.js +24 -8
  77. package/lib/providers/ollama-web.js +29 -11
  78. package/lib/providers/ollama.js +129 -66
  79. package/lib/providers/pool-family.js +11 -3
  80. package/lib/providers/pool-health.js +28 -8
  81. package/lib/providers/pool-usage.js +16 -9
  82. package/lib/providers/pool.d.ts +8 -0
  83. package/lib/providers/pool.js +81 -42
  84. package/lib/providers/rate-limit.js +8 -2
  85. package/lib/providers/replay.js +40 -17
  86. package/lib/providers/reset-redemption.js +18 -6
  87. package/lib/tools/image-generate.js +123 -65
  88. package/lib/tools/registration.js +5 -2
  89. package/lib/tools/video-generate.js +51 -31
  90. package/lib/tools/x-search.js +50 -22
  91. package/lib/translate/anthropic.js +85 -37
  92. package/lib/translate/antigravity-schema.js +71 -28
  93. package/lib/translate/antigravity-thinking.js +22 -9
  94. package/lib/translate/antigravity.js +141 -50
  95. package/lib/translate/chat-completions.js +67 -23
  96. package/lib/translate/resolved.js +55 -21
  97. package/lib/translate/responses.js +69 -29
  98. package/lib/translate/sse.js +4 -1
  99. package/package.json +26 -4
@@ -1,10 +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
4
  /** Providers whose runtime honours a local context-window override. */
5
5
  const supportsContext = (provider) => provider === 'codex' || provider === 'claude';
6
6
  const border = '0.5px solid var(--dsw-alias-border-l2, #ddd)';
7
- 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
+ };
8
13
  /**
9
14
  * Embedded section of the account manager dialog. It owns the model draft
10
15
  * (visibility, effort, context, tools) but not its submission: the dialog
@@ -21,12 +26,19 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
21
26
  const [dirty, setDirty] = useState(false);
22
27
  const [error, setError] = useState('');
23
28
  const generation = useRef(0);
24
- useEffect(() => () => { generation.current++; }, []);
25
- 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 */
26
38
  function reset(data) {
27
39
  setCatalog(data);
28
40
  setDraft(data.settings);
29
- setEfforts(Object.fromEntries(data.models.map(model => [model.id, model.configured ?? ''])));
41
+ setEfforts(Object.fromEntries(data.models.map((model) => [model.id, model.configured ?? ''])));
30
42
  setContexts(Object.fromEntries(Object.entries(data.settings.contextWindows ?? {}).map(([id, value]) => [id, String(value)])));
31
43
  setDirty(false);
32
44
  }
@@ -35,7 +47,9 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
35
47
  setBusy(true);
36
48
  setError('');
37
49
  try {
38
- 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 });
39
53
  if (generation.current === request)
40
54
  reset(data);
41
55
  }
@@ -48,8 +62,16 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
48
62
  setBusy(false);
49
63
  }
50
64
  }
51
- function edit(next) { setDraft(next); setDirty(true); }
52
- 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 */
53
75
  useImperativeHandle(ref, () => ({
54
76
  collect() {
55
77
  if (!catalog || !dirty)
@@ -59,47 +81,106 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
59
81
  if (!text.trim())
60
82
  continue;
61
83
  const value = Number(text);
62
- if (!/^\d+$/.test(text.trim()) || !Number.isSafeInteger(value) || value <= 0) {
84
+ if (!/^\d+$/.test(text.trim()) ||
85
+ !Number.isSafeInteger(value) ||
86
+ value <= 0) {
63
87
  setError(t('modelsContextInvalid', { model }));
64
88
  return undefined;
65
89
  }
66
90
  windows[model] = value;
67
91
  }
68
92
  setError('');
69
- const { accounts: _accounts, ...settings } = { ...draft, ...(supportsContext(provider) ? { contextWindows: windows } : {}) };
93
+ const { accounts: _accounts, ...settings } = {
94
+ ...draft,
95
+ ...(supportsContext(provider) ? { contextWindows: windows } : {}),
96
+ };
70
97
  return {
71
98
  settings,
72
- efforts: catalog.models.flatMap(model => {
99
+ efforts: catalog.models.flatMap((model) => {
73
100
  const effort = efforts[model.id] ?? '';
74
- 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 }];
75
105
  }),
76
106
  };
77
107
  },
78
- }), [catalog, dirty, draft, contexts, efforts, provider]);
108
+ }), [catalog, dirty, draft, contexts, efforts, provider, t]);
79
109
  const allModels = catalog?.models ?? [];
80
- const known = new Set(allModels.map(model => model.id));
81
- const missing = (draft.visibleModels ?? []).filter(id => !known.has(id)).map(id => ({ id, name: id }));
82
- const models = [...allModels, ...missing].filter(model => `${model.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()));
83
- const selected = new Set(draft.visibleModels ?? allModels.map(model => model.id));
84
- 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) => {
85
130
  const next = { ...draft };
86
131
  if (event.target.checked)
87
132
  delete next.visibleModels;
88
133
  else
89
- next.visibleModels = allModels.map(model => model.id);
134
+ next.visibleModels = allModels.map((model) => model.id);
90
135
  edit(next);
91
- } }), " ", 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) => {
92
150
  const next = new Set(selected);
93
151
  if (event.target.checked)
94
152
  next.add(model.id);
95
153
  else
96
154
  next.delete(model.id);
97
155
  edit({ ...draft, visibleModels: [...next] });
98
- } }), " ", 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 => {
99
- 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
+ });
100
161
  setDirty(true);
101
- }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured && !model.efforts?.some(effort => effort.id === model.configured) &&
102
- _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) && 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 }) })] }), 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({
103
- ...draft, tools: { ...draft.tools, [tool]: event.target.checked },
104
- }) }), " ", 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)))] }))] }))] })] }));
105
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;