dsh-subscriptions 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +73 -2
- package/lib/auth/rpc.js +53 -3
- package/lib/client/ExternalUsageCards.js +1 -1
- package/lib/client/OllamaCard.d.ts +13 -0
- package/lib/client/OllamaCard.js +76 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +246 -0
- package/lib/client/ProviderModelEditor.js +4 -2
- package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
- package/lib/client/SubscriptionUsageBadge.js +3 -2
- package/lib/client/SubscriptionsSection.d.ts +2 -74
- package/lib/client/SubscriptionsSection.js +4 -67
- package/lib/client/UsageMeter.js +11 -7
- package/lib/client/locales.d.ts +68 -0
- package/lib/client/locales.js +77 -0
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +246 -0
- package/lib/client/usage-pace.d.ts +7 -0
- package/lib/client/usage-pace.js +15 -0
- package/lib/client.js +1385 -244
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +25 -0
- package/lib/index.js +11060 -9733
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/provider-settings.d.ts +2 -1
- package/lib/provider-settings.js +6 -4
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/claude.d.ts +14 -1
- package/lib/providers/claude.js +25 -13
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.js +7 -7
- package/lib/providers/common.d.ts +27 -0
- package/lib/providers/common.js +70 -0
- package/lib/providers/copilot.js +3 -3
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage-controller.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +19 -5
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +93 -8
- package/lib/providers/grok.js +14 -9
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/ollama-settings.d.ts +90 -0
- package/lib/providers/ollama-settings.js +197 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +213 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +545 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/resolved.d.ts +5 -2
- package/lib/translate/resolved.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { callSubscriptionsAuth } from './subscriptions-rpc.js';
|
|
4
|
+
import { contextLabel, effortOptions, moveModel, parseContextInput, renameModel, setModelContext, setModelEffort, tickModel, tickModels, untickModel, untickModels, } from './ollama-draft.js';
|
|
5
|
+
import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
|
|
6
|
+
import { providerSettingsCss } from './provider-settings-styles.js';
|
|
7
|
+
const border = '0.5px solid var(--dsw-alias-border-l2)';
|
|
8
|
+
const stack = { display: 'grid', gap: 12, minWidth: 0 };
|
|
9
|
+
const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
|
|
10
|
+
function message(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
/** `vision · thinking · context 262K`, dropping whatever the model does not advertise. */
|
|
14
|
+
function capabilities(t, model) {
|
|
15
|
+
const context = contextLabel(model.contextWindow);
|
|
16
|
+
return [
|
|
17
|
+
model.vision === true ? t('ollamaCapVision') : false,
|
|
18
|
+
model.thinking === true ? t('ollamaCapThinking') : false,
|
|
19
|
+
context === undefined ? false : t('ollamaContext', { tokens: context }),
|
|
20
|
+
].filter(Boolean).join(' · ');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ollama Cloud settings dialog. It owns one draft (model list, web toggle,
|
|
24
|
+
* base URL and credential reference) that Save writes in a single
|
|
25
|
+
* `ollamaSetSettings` call, so a failure keeps every edit. Fetching the cloud
|
|
26
|
+
* catalog is a read-only side trip, and a key typed for the fetch never
|
|
27
|
+
* leaves this component. The model list follows the shared model editor: a
|
|
28
|
+
* filter, select all/none over the filtered rows, and one checkbox per
|
|
29
|
+
* model, where ticking edits the draft and enables Save. Ollama adds what the
|
|
30
|
+
* shared editor lacks: a display name, ordering, and a local context window.
|
|
31
|
+
* Unticking keeps the row's full settings in a dialog-local stash, so ticking
|
|
32
|
+
* it again restores them even when the cloud listing no longer returns it.
|
|
33
|
+
*/
|
|
34
|
+
export function OllamaSettingsManager({ rpc, t, onClose, onSaved }) {
|
|
35
|
+
const dialog = useRef(null);
|
|
36
|
+
const title = useId();
|
|
37
|
+
const description = useId();
|
|
38
|
+
const [view, setView] = useState();
|
|
39
|
+
const [draft, setDraft] = useState({});
|
|
40
|
+
const [dirty, setDirty] = useState(false);
|
|
41
|
+
const [loading, setLoading] = useState(true);
|
|
42
|
+
const [saving, setSaving] = useState(false);
|
|
43
|
+
const [error, setError] = useState('');
|
|
44
|
+
const [saveError, setSaveError] = useState('');
|
|
45
|
+
const [attempt, setAttempt] = useState(0);
|
|
46
|
+
const [apiKey, setApiKey] = useState('');
|
|
47
|
+
const [candidates, setCandidates] = useState();
|
|
48
|
+
const [fetching, setFetching] = useState(false);
|
|
49
|
+
const [fetchError, setFetchError] = useState('');
|
|
50
|
+
const [query, setQuery] = useState('');
|
|
51
|
+
const [expanded, setExpanded] = useState(new Set());
|
|
52
|
+
const [stash, setStash] = useState(new Map());
|
|
53
|
+
const [contextText, setContextText] = useState({});
|
|
54
|
+
const alive = useRef(true);
|
|
55
|
+
const saveLock = useRef(false);
|
|
56
|
+
const fetchLock = useRef(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
alive.current = true;
|
|
59
|
+
const element = dialog.current;
|
|
60
|
+
const previous = document.activeElement;
|
|
61
|
+
element.showModal();
|
|
62
|
+
return () => {
|
|
63
|
+
alive.current = false;
|
|
64
|
+
element.close();
|
|
65
|
+
if (previous instanceof HTMLElement && previous.isConnected)
|
|
66
|
+
previous.focus();
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let current = true;
|
|
71
|
+
setLoading(true);
|
|
72
|
+
setError('');
|
|
73
|
+
void callSubscriptionsAuth(rpc, 'ollamaSettings', {}).then(data => {
|
|
74
|
+
if (!current)
|
|
75
|
+
return;
|
|
76
|
+
setView(data);
|
|
77
|
+
// An unset store value inherits the deployed config: the checkbox
|
|
78
|
+
// shows the effective state, and ticking it writes the override.
|
|
79
|
+
setDraft({ ...data.settings });
|
|
80
|
+
setStash(new Map());
|
|
81
|
+
setContextText({});
|
|
82
|
+
setDirty(false);
|
|
83
|
+
}).catch(failure => {
|
|
84
|
+
if (current)
|
|
85
|
+
setError(t('ollamaModelsLoadFailed', { message: message(failure) }));
|
|
86
|
+
}).finally(() => { if (current)
|
|
87
|
+
setLoading(false); });
|
|
88
|
+
return () => { current = false; };
|
|
89
|
+
}, [rpc, attempt]);
|
|
90
|
+
function edit(next) {
|
|
91
|
+
setDraft(next);
|
|
92
|
+
setDirty(true);
|
|
93
|
+
}
|
|
94
|
+
/** Fetch the cloud catalog; a failure keeps the previous list and the draft. */
|
|
95
|
+
async function fetchCandidates() {
|
|
96
|
+
if (fetchLock.current)
|
|
97
|
+
return;
|
|
98
|
+
fetchLock.current = true;
|
|
99
|
+
setFetching(true);
|
|
100
|
+
setFetchError('');
|
|
101
|
+
try {
|
|
102
|
+
const value = await callSubscriptionsAuth(rpc, 'ollamaModels', { ...(apiKey ? { apiKey } : {}), ...(draft.baseURL ? { baseURL: draft.baseURL } : {}) });
|
|
103
|
+
if (!alive.current)
|
|
104
|
+
return;
|
|
105
|
+
setCandidates(value.models);
|
|
106
|
+
}
|
|
107
|
+
catch (failure) {
|
|
108
|
+
if (alive.current)
|
|
109
|
+
setFetchError(t('ollamaFetchFailed', { message: message(failure) }));
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
fetchLock.current = false;
|
|
113
|
+
if (alive.current)
|
|
114
|
+
setFetching(false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Save the whole draft; a failure keeps the dialog open with every edit. */
|
|
118
|
+
async function save() {
|
|
119
|
+
if (saveLock.current)
|
|
120
|
+
return;
|
|
121
|
+
saveLock.current = true;
|
|
122
|
+
setSaving(true);
|
|
123
|
+
setSaveError('');
|
|
124
|
+
try {
|
|
125
|
+
await callSubscriptionsAuth(rpc, 'ollamaSetSettings', { settings: draft });
|
|
126
|
+
if (alive.current) {
|
|
127
|
+
onSaved();
|
|
128
|
+
onClose();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (failure) {
|
|
132
|
+
if (alive.current)
|
|
133
|
+
setSaveError(t('ollamaModelsSaveFailed', { message: message(failure) }));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
saveLock.current = false;
|
|
137
|
+
if (alive.current)
|
|
138
|
+
setSaving(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Drop every stored override, then reload the draft from the deployment values. */
|
|
142
|
+
async function resetDefaults() {
|
|
143
|
+
if (saveLock.current)
|
|
144
|
+
return;
|
|
145
|
+
if (!window.confirm(t('ollamaResetConfirm')))
|
|
146
|
+
return;
|
|
147
|
+
saveLock.current = true;
|
|
148
|
+
setSaving(true);
|
|
149
|
+
setSaveError('');
|
|
150
|
+
try {
|
|
151
|
+
await callSubscriptionsAuth(rpc, 'ollamaSetSettings', { settings: {} });
|
|
152
|
+
if (alive.current) {
|
|
153
|
+
setAttempt(value => value + 1);
|
|
154
|
+
onSaved();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (failure) {
|
|
158
|
+
if (alive.current)
|
|
159
|
+
setSaveError(t('ollamaModelsSaveFailed', { message: message(failure) }));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
saveLock.current = false;
|
|
163
|
+
if (alive.current)
|
|
164
|
+
setSaving(false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function editOverride(field, value) {
|
|
168
|
+
const next = { ...draft };
|
|
169
|
+
if (value.trim())
|
|
170
|
+
next[field] = value;
|
|
171
|
+
else
|
|
172
|
+
delete next[field];
|
|
173
|
+
edit(next);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* One list for both sources, like the shared editor: the ticked models in
|
|
177
|
+
* draft order first (so Move up/down is visible), then the unticked ones
|
|
178
|
+
* (stashed in this dialog, or fetched and not yet adopted). A kept model
|
|
179
|
+
* the fetch did not return stays listed, ticked or not.
|
|
180
|
+
*/
|
|
181
|
+
const fetchedById = new Map((candidates ?? []).map(candidate => [candidate.id, candidate]));
|
|
182
|
+
const present = new Set((draft.models ?? []).map(model => model.id));
|
|
183
|
+
const asRow = (model, ticked) => {
|
|
184
|
+
const fetched = fetchedById.get(model.id);
|
|
185
|
+
// The latest fetch outranks the stored snapshot, matching the runtime:
|
|
186
|
+
// discovery is the ceiling and the snapshot only stands in without it.
|
|
187
|
+
const contextWindow = fetched?.contextWindow ?? model.contextWindow;
|
|
188
|
+
return {
|
|
189
|
+
id: model.id, name: model.name ?? fetched?.name ?? model.id, ticked,
|
|
190
|
+
vision: model.vision === true || fetched?.vision === true,
|
|
191
|
+
thinking: model.thinking === true || fetched?.thinking === true,
|
|
192
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
193
|
+
efforts: model.efforts ?? fetched?.efforts ?? [],
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const rows = [
|
|
197
|
+
...(draft.models ?? []).map(model => asRow(model, true)),
|
|
198
|
+
...[...stash.values()].filter(model => !present.has(model.id)).map(model => asRow(model, false)),
|
|
199
|
+
...(candidates ?? [])
|
|
200
|
+
.filter(candidate => !present.has(candidate.id) && !stash.has(candidate.id))
|
|
201
|
+
.map(candidate => asRow({ id: candidate.id }, false)),
|
|
202
|
+
];
|
|
203
|
+
const shown = rows.filter(row => `${row.name} ${row.id}`.toLowerCase().includes(query.trim().toLowerCase()));
|
|
204
|
+
/** Rename and default-effort edits ride the tested draft helpers. */
|
|
205
|
+
function rename(id, name) {
|
|
206
|
+
edit({ ...draft, models: renameModel(draft.models ?? [], id, name) });
|
|
207
|
+
}
|
|
208
|
+
function setEffort(id, effort) {
|
|
209
|
+
edit({ ...draft, models: setModelEffort(draft.models ?? [], id, effort) });
|
|
210
|
+
}
|
|
211
|
+
/** Keep the typed text; only a valid number (or blank) reaches the draft. */
|
|
212
|
+
function setContext(id, text) {
|
|
213
|
+
setContextText(current => ({ ...current, [id]: text }));
|
|
214
|
+
const tokens = parseContextInput(text);
|
|
215
|
+
if (tokens !== null)
|
|
216
|
+
edit({ ...draft, models: setModelContext(draft.models ?? [], id, tokens) });
|
|
217
|
+
}
|
|
218
|
+
function applyTicks(next) {
|
|
219
|
+
setStash(next.stash);
|
|
220
|
+
edit({ ...draft, models: next.models });
|
|
221
|
+
}
|
|
222
|
+
const invalidContext = Object.entries(contextText)
|
|
223
|
+
.filter(([id, text]) => present.has(id) && parseContextInput(text) === null).map(([id]) => id);
|
|
224
|
+
return _jsxs("dialog", { className: "dsh-subscription-manager", ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: event => { if (saveLock.current)
|
|
225
|
+
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('ollamaManageTitle') }), _jsx("button", { type: "button", autoFocus: true, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, className: "dsh-subscription-hint", children: t('ollamaManageHint') }), view?.configOverridesModels === true && _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaCatalogOverride') }), error && _jsx("p", { role: "alert", children: error }), loading && _jsx("p", { role: "status", className: "dsh-subscription-hint", children: t('modelDefaultsLoading') }), !loading && !view && _jsx("button", { type: "button", onClick: () => setAttempt(value => value + 1), children: t('modelDefaultsRetry') }), view && _jsxs("fieldset", { disabled: saving, style: { ...stack, border: 0, margin: 0, padding: 0, minWidth: 0 }, children: [_jsxs("div", { style: { borderTop: border, paddingTop: 12, display: 'grid', gap: 12 }, children: [_jsx("h3", { style: { margin: 0 }, children: t('modelsEdit') }), _jsxs("div", { style: actions, children: [_jsx("input", { type: "password", autoComplete: "off", style: { flex: '1 1 180px' }, "aria-label": t('ollamaKeyPlaceholder'), placeholder: t('ollamaKeyPlaceholder'), value: apiKey, onChange: event => setApiKey(event.target.value) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: fetching, onClick: () => { void fetchCandidates(); }, children: fetching ? t('ollamaFetching') : t('ollamaFetch') }), candidates !== undefined && _jsx("span", { className: "dsh-subscription-hint", children: t('ollamaCandidatesCount', { count: candidates.length }) })] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaKeyHint') }), fetchError && _jsx("p", { role: "alert", children: fetchError }), candidates?.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaCandidatesEmpty') }), _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: () => applyTicks(tickModels(draft.models ?? [], stash, candidates ?? [], shown.map(row => row.id))), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => applyTicks(untickModels(draft.models ?? [], stash, shown.map(row => row.id))), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { maxHeight: 360, overflowY: 'auto', display: 'grid', gap: 8 }, children: [shown.map(row => {
|
|
226
|
+
const stored = (draft.models ?? []).find(entry => entry.id === row.id);
|
|
227
|
+
const index = (draft.models ?? []).findIndex(entry => entry.id === row.id);
|
|
228
|
+
const open = expanded.has(row.id);
|
|
229
|
+
const details = `${row.id}-details`;
|
|
230
|
+
const efforts = stored === undefined ? [] : effortOptions(stored, fetchedById.get(row.id)?.efforts, view.models.find(entry => entry.id === row.id)?.efforts);
|
|
231
|
+
const contextProblem = invalidContext.includes(row.id);
|
|
232
|
+
return _jsxs("div", { style: { borderBottom: border, padding: '8px 2px', display: 'grid', gap: 8 }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: row.ticked, onChange: event => {
|
|
233
|
+
applyTicks(event.target.checked
|
|
234
|
+
? tickModel(draft.models ?? [], stash, candidates ?? [], row.id)
|
|
235
|
+
: untickModel(draft.models ?? [], stash, row.id));
|
|
236
|
+
} }), " ", row.name, " ", _jsx("span", { style: styles.status, children: capabilities(t, row) })] }), stored !== undefined && _jsxs("div", { style: actions, children: [row.thinking && efforts.length > 0 && _jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { "aria-label": `${row.name} ${t('modelDefaultsTitle')}`, value: stored.defaultEffort ?? '', onChange: event => setEffort(row.id, event.target.value), children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), efforts.map(effort => _jsx("option", { value: effort.id, disabled: effort.unavailable === true, children: effort.unavailable === true ? `${effort.name} (${t('modelsUnavailable')})` : effort.name }, effort.id))] })] }), row.thinking && efforts.length === 0 && _jsx("small", { children: t('ollamaEffortsUnknown') }), _jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", "aria-invalid": contextProblem, value: contextText[row.id] ?? (stored.contextOverride === undefined ? '' : String(stored.contextOverride)), "aria-label": `${row.name} ${t('modelsContext')}`, placeholder: row.contextWindow === undefined ? '' : String(row.contextWindow), onChange: event => setContext(row.id, event.target.value) })] }), row.contextWindow !== undefined && _jsx("small", { children: t('modelsContextBounds', { default: row.contextWindow, max: row.contextWindow }) }), _jsxs("button", { type: "button", className: "dsh-subscription-quiet", "aria-expanded": open, "aria-controls": details, "aria-label": `${row.name} ${t('ollamaRowDetails')}`, onClick: () => {
|
|
237
|
+
const next = new Set(expanded);
|
|
238
|
+
if (open)
|
|
239
|
+
next.delete(row.id);
|
|
240
|
+
else
|
|
241
|
+
next.add(row.id);
|
|
242
|
+
setExpanded(next);
|
|
243
|
+
}, children: [t('ollamaRowDetails'), " ", open ? '\u25BE' : '\u25B8'] })] }), contextProblem && _jsx("p", { role: "alert", children: t('modelsContextInvalid', { model: row.name }) }), stored !== undefined && open && _jsxs("div", { id: details, style: actions, children: [_jsxs("label", { style: actions, children: [t('ollamaDisplayName'), _jsx("input", { value: stored.name ?? '', placeholder: row.id, "aria-label": `${row.name} ${t('ollamaDisplayName')}`, onChange: event => rename(row.id, event.target.value), onBlur: event => rename(row.id, event.target.value.trim()) })] }), _jsx("button", { type: "button", disabled: index === 0, "aria-label": `${row.name} ${t('ollamaMoveUp')}`, onClick: () => edit({ ...draft, models: moveModel(draft.models ?? [], row.id, -1) }), children: t('ollamaMoveUp') }), _jsx("button", { type: "button", disabled: index === (draft.models ?? []).length - 1, "aria-label": `${row.name} ${t('ollamaMoveDown')}`, onClick: () => edit({ ...draft, models: moveModel(draft.models ?? [], row.id, 1) }), children: t('ollamaMoveDown') })] })] }, row.id);
|
|
244
|
+
}), shown.length === 0 && _jsx("span", { className: "dsh-subscription-hint", children: query.trim() ? t('modelDefaultsFilterEmpty', { query }) : t('ollamaModelsEmpty') })] }), _jsx("small", { children: t('modelsContextHint') })] }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('ollamaBaseURL') }), _jsx("input", { value: draft.baseURL ?? '', placeholder: view.effective.baseURL, onChange: event => editOverride('baseURL', event.target.value) })] }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('ollamaApiKeyRef') }), _jsx("input", { value: draft.apiKeyRef ?? '', placeholder: view.effective.apiKeyRef, onChange: event => editOverride('apiKeyRef', event.target.value) })] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaConnectionHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: (draft.web ?? view.effective.web) === true, onChange: event => edit({ ...draft, web: event.target.checked }) }), " ", t('modelsWebSearch')] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaWebHint') })] }), _jsxs("footer", { style: { ...stack, gap: 8, borderTop: border, padding: '14px 0 8px',
|
|
245
|
+
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", style: { marginRight: 'auto' }, disabled: loading || saving, onClick: () => { void resetDefaults(); }, children: t('ollamaResetDefaults') }), _jsx("button", { type: "button", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: loading || saving || !dirty || invalidContext.length > 0, onClick: () => { void save(); }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] })] });
|
|
246
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
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
7
|
const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
|
|
6
8
|
/**
|
|
@@ -64,7 +66,7 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
|
|
|
64
66
|
windows[model] = value;
|
|
65
67
|
}
|
|
66
68
|
setError('');
|
|
67
|
-
const { accounts: _accounts, ...settings } = { ...draft, ...(provider
|
|
69
|
+
const { accounts: _accounts, ...settings } = { ...draft, ...(supportsContext(provider) ? { contextWindows: windows } : {}) };
|
|
68
70
|
return {
|
|
69
71
|
settings,
|
|
70
72
|
efforts: catalog.models.flatMap(model => {
|
|
@@ -97,7 +99,7 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
|
|
|
97
99
|
setEfforts({ ...efforts, [model.id]: event.target.value });
|
|
98
100
|
setDirty(true);
|
|
99
101
|
}, 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
|
|
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({
|
|
101
103
|
...draft, tools: { ...draft.tools, [tool]: event.target.checked },
|
|
102
104
|
}) }), " ", t(tool === 'image_generate' ? 'modelsImage' : tool === 'video_generate' ? 'modelsVideo' : tool === 'web_search' ? 'modelsWebSearch' : 'modelsSearch')] }, tool))] })] })] })] });
|
|
103
105
|
});
|
|
@@ -48,7 +48,7 @@ export interface ProviderUsageDisplay {
|
|
|
48
48
|
/** The account the collapsed pill reads: the default one, else the first listed. */
|
|
49
49
|
export declare function pillAccountOf(d: ProviderUsageDisplay): AccountUsageDisplay;
|
|
50
50
|
/** Brand display names (short form for the compact badge). */
|
|
51
|
-
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn';
|
|
51
|
+
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn' | 'ollama-cloud';
|
|
52
52
|
export interface UsageRosterEntry {
|
|
53
53
|
provider: BadgeProvider;
|
|
54
54
|
account: AccountStatus;
|
|
@@ -64,6 +64,7 @@ const PROVIDER_NAMES = {
|
|
|
64
64
|
'kimi-coding': 'Kimi Code',
|
|
65
65
|
'minimax': 'MiniMax',
|
|
66
66
|
'minimax-cn': 'MiniMax CN',
|
|
67
|
+
'ollama-cloud': 'Ollama Cloud',
|
|
67
68
|
};
|
|
68
69
|
/** Key of one account's cached windows: `provider:accountKey`. */
|
|
69
70
|
function usageKeyOf(provider, account) {
|
|
@@ -127,7 +128,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
127
128
|
}
|
|
128
129
|
if (external.status === 'fulfilled') {
|
|
129
130
|
for (const [source, provider] of [
|
|
130
|
-
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'],
|
|
131
|
+
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'], ['ollama-cloud', 'ollama-cloud'],
|
|
131
132
|
]) {
|
|
132
133
|
refreshed.add(provider);
|
|
133
134
|
if (external.value[source]?.configured)
|
|
@@ -141,7 +142,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
141
142
|
export async function usageOf(rpc, { provider, account }) {
|
|
142
143
|
if (provider === 'cursor-subscription')
|
|
143
144
|
return callSubscriptionsAuth(rpc, 'cursorUsage', {});
|
|
144
|
-
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn') {
|
|
145
|
+
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn' || provider === 'ollama-cloud') {
|
|
145
146
|
return callSubscriptionsAuth(rpc, 'externalUsage', { source: provider === 'kimi-coding' ? 'kimi-code' : provider });
|
|
146
147
|
}
|
|
147
148
|
return callSubscriptionsAuth(rpc, 'usage', { provider, account: account.key });
|
|
@@ -33,6 +33,8 @@ export interface UsageWindow {
|
|
|
33
33
|
startsAt?: number;
|
|
34
34
|
windowDurationMs?: number;
|
|
35
35
|
fixedWindow?: boolean;
|
|
36
|
+
/** Display-only assumed start; the marker rides it, never pace. */
|
|
37
|
+
displayStartsAt?: number;
|
|
36
38
|
}
|
|
37
39
|
/** `usage` endpoint value: the node half owns this shape. */
|
|
38
40
|
export interface ProviderUsage {
|
|
@@ -64,23 +66,6 @@ export interface ResetCreditView {
|
|
|
64
66
|
/** Epoch milliseconds until which the grant is visible but not usable. */
|
|
65
67
|
cooldownUntil?: number;
|
|
66
68
|
}
|
|
67
|
-
/** One model's default-effort picker state as answered by `modelDefaults`. */
|
|
68
|
-
export interface ModelDefaultView {
|
|
69
|
-
id: string;
|
|
70
|
-
name: string;
|
|
71
|
-
/** Advertised effort levels, in catalog order (empty when the model has no reasoning). */
|
|
72
|
-
efforts: {
|
|
73
|
-
id: string;
|
|
74
|
-
name: string;
|
|
75
|
-
}[];
|
|
76
|
-
/** The user-configured default effort, when set. */
|
|
77
|
-
configured?: string;
|
|
78
|
-
}
|
|
79
|
-
/** `modelDefaults` endpoint value: one provider's picker state. */
|
|
80
|
-
export interface ModelDefaultsCatalog {
|
|
81
|
-
provider: SubscriptionProvider;
|
|
82
|
-
models: ModelDefaultView[];
|
|
83
|
-
}
|
|
84
69
|
/** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
|
|
85
70
|
export interface SubscriptionsSectionInjected {
|
|
86
71
|
/** Generic logical-RPC caller over the Connection transport. */
|
|
@@ -102,63 +87,6 @@ export type SubscriptionsSectionProps = Partial<SubscriptionsSectionInjected>;
|
|
|
102
87
|
export declare function clientVersionText(t: SubscriptionsSectionInjected['t'], clientVersion: ProviderStatus['clientVersion']): string;
|
|
103
88
|
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
104
89
|
export { usageBarColor } from './usage-pace.js';
|
|
105
|
-
/** What one provider's collapsible default-effort section renders. */
|
|
106
|
-
export interface ModelDefaultsView {
|
|
107
|
-
/** Models with reasoning levels, after the name filter — one row each. */
|
|
108
|
-
shown: ModelDefaultView[];
|
|
109
|
-
/** Models with reasoning levels before filtering (the header total). */
|
|
110
|
-
total: number;
|
|
111
|
-
/** How many of those carry a user override (the header count). */
|
|
112
|
-
overridden: number;
|
|
113
|
-
/** Models without reasoning levels: one count line, never a row each. */
|
|
114
|
-
withoutEfforts: number;
|
|
115
|
-
/** Whether the list is long enough to deserve a filter box. */
|
|
116
|
-
showFilter: boolean;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Derive one provider's default-effort section from its catalog and filter.
|
|
120
|
-
* Pure so the collapsed-header counts and the filter stay testable without a
|
|
121
|
-
* DOM: rows come only from models that advertise levels, the count of the rest
|
|
122
|
-
* rides as one line, and the filter matches display name or model id.
|
|
123
|
-
* @param models - the provider's catalog models, or undefined while loading.
|
|
124
|
-
* @param filter - the raw filter input (trimmed and lowercased here).
|
|
125
|
-
* @returns the section's rows and header counts.
|
|
126
|
-
*/
|
|
127
|
-
export declare function deriveModelDefaultsView(models: readonly ModelDefaultView[] | undefined, filter: string): ModelDefaultsView;
|
|
128
|
-
/** Inputs of the default-effort fetch decision (see {@link shouldFetchModelDefaults}). */
|
|
129
|
-
export interface ModelDefaultsFetchInput {
|
|
130
|
-
/** Providers that currently have at least one account. */
|
|
131
|
-
loggedIn: readonly SubscriptionProvider[];
|
|
132
|
-
/** Providers whose disclosure is open. */
|
|
133
|
-
open: readonly SubscriptionProvider[];
|
|
134
|
-
/** The account signature the last completed fetch was answered for. */
|
|
135
|
-
loadedFor: string | undefined;
|
|
136
|
-
/** The account signature of the current status snapshot. */
|
|
137
|
-
signature: string;
|
|
138
|
-
/** Whether the last attempt failed (a failure latches until Retry). */
|
|
139
|
-
failed: boolean;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Whether the default-effort catalog needs (re)fetching.
|
|
143
|
-
*
|
|
144
|
-
* Fetching is gated on an *attempt* signature rather than on the payload
|
|
145
|
-
* being empty: an empty answer is a legitimate result (a narrowed
|
|
146
|
-
* `config.providers`, or a catalog that is momentarily unavailable), and
|
|
147
|
-
* treating it as "not loaded yet" re-ran this effect forever. The signature
|
|
148
|
-
* also covers the accounts, so logging a second provider in refetches
|
|
149
|
-
* instead of leaving that card on the previous answer.
|
|
150
|
-
* @param input - the decision inputs.
|
|
151
|
-
* @returns true when the caller should start a fetch.
|
|
152
|
-
*/
|
|
153
|
-
export declare function shouldFetchModelDefaults(input: ModelDefaultsFetchInput): boolean;
|
|
154
|
-
/**
|
|
155
|
-
* Stable signature of the accounts a catalog answer depends on. A change
|
|
156
|
-
* means a previous answer is stale (an account arrived or left), so the next
|
|
157
|
-
* open disclosure refetches.
|
|
158
|
-
* @param statuses - the per-provider status snapshot.
|
|
159
|
-
* @returns a signature string, stable across renders with equal accounts.
|
|
160
|
-
*/
|
|
161
|
-
export declare function modelDefaultsSignature(statuses: Partial<Record<SubscriptionProvider, ProviderStatus>>): string;
|
|
162
90
|
/**
|
|
163
91
|
* The Subscriptions settings page component.
|
|
164
92
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Subscriptions settings section: one card per subscription provider with an
|
|
4
|
-
* OAuth login/logout flow driven by the node half's
|
|
5
|
-
*
|
|
4
|
+
* OAuth login/logout flow driven by the node half's
|
|
5
|
+
* `/api/subscriptions-auth.<endpoint>` RPC routes. Login state lives server-side; the page polls `status` only while
|
|
6
6
|
* a login attempt is busy, so an idle page never polls. All state is local
|
|
7
7
|
* React state — the page has no store.
|
|
8
8
|
*
|
|
@@ -19,6 +19,7 @@ import { ProviderAccountManager } from './ProviderAccountManager.js';
|
|
|
19
19
|
import { providerSettingsCss } from './provider-settings-styles.js';
|
|
20
20
|
import { ExternalUsageCards } from './ExternalUsageCards.js';
|
|
21
21
|
import { CursorCard } from './CursorCard.js';
|
|
22
|
+
import { OllamaCard } from './OllamaCard.js';
|
|
22
23
|
import { UsageMeter } from './UsageMeter.js';
|
|
23
24
|
import { ResetCreditsDisclosure, ResetCreditsErrorLine, showsResetCredits } from './reset-credits-view.js';
|
|
24
25
|
import { displayUsedPercent } from './usage-pace.js';
|
|
@@ -29,11 +30,6 @@ import { callSubscriptionsAuth, SubscriptionsAuthError } from './subscriptions-r
|
|
|
29
30
|
export { callSubscriptionsAuth } from './subscriptions-rpc.js';
|
|
30
31
|
/** Poll cadence while a provider login attempt is busy. */
|
|
31
32
|
const POLL_INTERVAL_MS = 2000;
|
|
32
|
-
/**
|
|
33
|
-
* Model count above which the expanded default-effort list also offers a name
|
|
34
|
-
* filter; below it the list is short enough to scan.
|
|
35
|
-
*/
|
|
36
|
-
const MODEL_FILTER_THRESHOLD = 8;
|
|
37
33
|
const CLIENT_VERSION_SOURCES = {
|
|
38
34
|
npm: 'clientVersionNpm',
|
|
39
35
|
local: 'clientVersionLocal',
|
|
@@ -192,65 +188,6 @@ function usageWindowLabel(t, window) {
|
|
|
192
188
|
}
|
|
193
189
|
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
194
190
|
export { usageBarColor } from './usage-pace.js';
|
|
195
|
-
/**
|
|
196
|
-
* Derive one provider's default-effort section from its catalog and filter.
|
|
197
|
-
* Pure so the collapsed-header counts and the filter stay testable without a
|
|
198
|
-
* DOM: rows come only from models that advertise levels, the count of the rest
|
|
199
|
-
* rides as one line, and the filter matches display name or model id.
|
|
200
|
-
* @param models - the provider's catalog models, or undefined while loading.
|
|
201
|
-
* @param filter - the raw filter input (trimmed and lowercased here).
|
|
202
|
-
* @returns the section's rows and header counts.
|
|
203
|
-
*/
|
|
204
|
-
export function deriveModelDefaultsView(models, filter) {
|
|
205
|
-
const all = models ?? [];
|
|
206
|
-
const withEfforts = all.filter(model => model.efforts.length > 0);
|
|
207
|
-
const query = filter.trim().toLowerCase();
|
|
208
|
-
const shown = query === ''
|
|
209
|
-
? withEfforts
|
|
210
|
-
: withEfforts.filter(model => model.name.toLowerCase().includes(query)
|
|
211
|
-
|| model.id.toLowerCase().includes(query));
|
|
212
|
-
return {
|
|
213
|
-
shown,
|
|
214
|
-
total: withEfforts.length,
|
|
215
|
-
overridden: withEfforts.filter(model => model.configured !== undefined).length,
|
|
216
|
-
withoutEfforts: all.length - withEfforts.length,
|
|
217
|
-
showFilter: withEfforts.length > MODEL_FILTER_THRESHOLD,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Whether the default-effort catalog needs (re)fetching.
|
|
222
|
-
*
|
|
223
|
-
* Fetching is gated on an *attempt* signature rather than on the payload
|
|
224
|
-
* being empty: an empty answer is a legitimate result (a narrowed
|
|
225
|
-
* `config.providers`, or a catalog that is momentarily unavailable), and
|
|
226
|
-
* treating it as "not loaded yet" re-ran this effect forever. The signature
|
|
227
|
-
* also covers the accounts, so logging a second provider in refetches
|
|
228
|
-
* instead of leaving that card on the previous answer.
|
|
229
|
-
* @param input - the decision inputs.
|
|
230
|
-
* @returns true when the caller should start a fetch.
|
|
231
|
-
*/
|
|
232
|
-
export function shouldFetchModelDefaults(input) {
|
|
233
|
-
if (input.failed)
|
|
234
|
-
return false;
|
|
235
|
-
if (input.loggedIn.length === 0)
|
|
236
|
-
return false;
|
|
237
|
-
// Only an open disclosure pays for the per-model live resolve.
|
|
238
|
-
if (!input.open.some(provider => input.loggedIn.includes(provider)))
|
|
239
|
-
return false;
|
|
240
|
-
return input.loadedFor !== input.signature;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Stable signature of the accounts a catalog answer depends on. A change
|
|
244
|
-
* means a previous answer is stale (an account arrived or left), so the next
|
|
245
|
-
* open disclosure refetches.
|
|
246
|
-
* @param statuses - the per-provider status snapshot.
|
|
247
|
-
* @returns a signature string, stable across renders with equal accounts.
|
|
248
|
-
*/
|
|
249
|
-
export function modelDefaultsSignature(statuses) {
|
|
250
|
-
return PROVIDERS
|
|
251
|
-
.map(({ id }) => `${id}:${(statuses[id]?.accounts ?? []).map(account => account.key).sort().join(',')}`)
|
|
252
|
-
.join('|');
|
|
253
|
-
}
|
|
254
191
|
/**
|
|
255
192
|
* The Subscriptions settings page component.
|
|
256
193
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|
|
@@ -626,5 +563,5 @@ export function SubscriptionsSection(props) {
|
|
|
626
563
|
void prepareReset(account.key); },
|
|
627
564
|
} } : {}, ...resetAccount === account.key && resetMessage !== '' ? { statusMessage: resetMessage } : {} }))] }))] }, account.key));
|
|
628
565
|
}), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", onClick: () => { void cancel(id); }, 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: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
629
|
-
}), _jsx(CursorCard, { 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: { ...styles.modal, display: resetConfirmation ? 'flex' : 'none', margin: 'auto', color: 'var(--dsw-alias-label-primary)' }, children: resetConfirmation && _jsxs(_Fragment, { children: [_jsx("h3", { id: "subscriptions-reset-title", style: { ...styles.modalTitle, margin: 0 }, children: t('resetUseConfirmTitle') }), _jsx("p", { style: { ...styles.statusLine, overflowWrap: 'anywhere' }, children: statuses.codex?.accounts.find(a => a.key === resetConfirmation.account)?.account ?? resetConfirmation.account }), _jsxs("div", { style: { ...styles.usageMeta, gap: 12 }, children: [_jsx("span", { children: t('usageWeekly') }), _jsxs("strong", { children: [Math.round(resetConfirmation.weeklyUsedPercent), "%"] })] }), _jsx("div", { style: styles.usageMeta, children: _jsx("span", { children: resetConfirmation.creditExpiresAt === undefined ? t('resetCreditExpiryUnknown') : t('resetCreditExpires', { date: new Date(resetConfirmation.creditExpiresAt).toLocaleString() }) }) }), _jsx("p", { style: { ...styles.statusLine, margin: '4px 0' }, children: t('resetUseWarning') }), _jsxs("label", { style: { display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12 }, children: [_jsx("input", { type: "checkbox", checked: resetAcknowledged, onChange: e => setResetAcknowledged(e.target.checked) }), t('resetUseAcknowledge')] }), _jsxs("div", { className: "dsh-subscription-dialog-actions", style: { ...styles.actions, justifyContent: 'flex-end', marginTop: 8 }, children: [_jsx("button", { type: "button", autoFocus: true, onClick: () => setResetConfirmation(undefined), children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: !resetAcknowledged || resetBusy, onClick: () => { void consumeReset(); }, children: t('resetUseConfirm') })] })] }) })] }));
|
|
566
|
+
}), _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: { ...styles.modal, display: resetConfirmation ? 'flex' : 'none', margin: 'auto', color: 'var(--dsw-alias-label-primary)' }, children: resetConfirmation && _jsxs(_Fragment, { children: [_jsx("h3", { id: "subscriptions-reset-title", style: { ...styles.modalTitle, margin: 0 }, children: t('resetUseConfirmTitle') }), _jsx("p", { style: { ...styles.statusLine, overflowWrap: 'anywhere' }, children: statuses.codex?.accounts.find(a => a.key === resetConfirmation.account)?.account ?? resetConfirmation.account }), _jsxs("div", { style: { ...styles.usageMeta, gap: 12 }, children: [_jsx("span", { children: t('usageWeekly') }), _jsxs("strong", { children: [Math.round(resetConfirmation.weeklyUsedPercent), "%"] })] }), _jsx("div", { style: styles.usageMeta, children: _jsx("span", { children: resetConfirmation.creditExpiresAt === undefined ? t('resetCreditExpiryUnknown') : t('resetCreditExpires', { date: new Date(resetConfirmation.creditExpiresAt).toLocaleString() }) }) }), _jsx("p", { style: { ...styles.statusLine, margin: '4px 0' }, children: t('resetUseWarning') }), _jsxs("label", { style: { display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12 }, children: [_jsx("input", { type: "checkbox", checked: resetAcknowledged, onChange: e => setResetAcknowledged(e.target.checked) }), t('resetUseAcknowledge')] }), _jsxs("div", { className: "dsh-subscription-dialog-actions", style: { ...styles.actions, justifyContent: 'flex-end', marginTop: 8 }, children: [_jsx("button", { type: "button", autoFocus: true, onClick: () => setResetConfirmation(undefined), children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: !resetAcknowledged || resetBusy, onClick: () => { void consumeReset(); }, children: t('resetUseConfirm') })] })] }) })] }));
|
|
630
567
|
}
|
package/lib/client/UsageMeter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
3
|
+
import { displayStartPercent, elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
4
4
|
import { useUsageColorPreset } from './usage-color-preferences.js';
|
|
5
5
|
/** Singular/plural locale keys per countdown unit (zh maps both to one string). */
|
|
6
6
|
const UNIT_KEYS = {
|
|
@@ -20,11 +20,13 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
20
20
|
const valid = validUsage(quota.usedPercent);
|
|
21
21
|
const used = valid ? quota.usedPercent : 0;
|
|
22
22
|
const fresh = isUsageFresh(quota, { observedAt, stale }, current);
|
|
23
|
-
// The cursor is where the window stands in time
|
|
24
|
-
//
|
|
23
|
+
// The cursor is where the window stands in time: a provider-verified start
|
|
24
|
+
// through elapsedPercent, else the display-only assumption.
|
|
25
25
|
const elapsed = elapsedPercent(quota, current);
|
|
26
|
+
const marker = elapsed ?? displayStartPercent(quota, current);
|
|
26
27
|
// Comparing usage with elapsed time is meaningful only while the percentage
|
|
27
|
-
// is current
|
|
28
|
+
// is current AND the start is verified: a display-estimated start must not
|
|
29
|
+
// color the bar — elapsed already excludes it, so pace below is safe.
|
|
28
30
|
const pace = fresh ? elapsed : undefined;
|
|
29
31
|
const state = usageColorState(quota.usedPercent, pace, preset, fresh);
|
|
30
32
|
const lead = pace === undefined ? 0 : Math.round(used - pace);
|
|
@@ -34,7 +36,9 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
34
36
|
!valid ? '' : !fresh ? t('usageMeterStale') : state === 'red' ? t('usageMeterNearLimit')
|
|
35
37
|
: pace === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
|
|
36
38
|
: state === 'yellow' ? t('usageMeterAhead') : t('usageMeterNoWarning'),
|
|
37
|
-
elapsed
|
|
39
|
+
// The elapsed-time line is a timing fact: it shows whenever the window's
|
|
40
|
+
// interval is known, even for a stale or invalid reading.
|
|
41
|
+
marker === undefined ? '' : t('usageMeterPace', { elapsed: Math.round(marker) }),
|
|
38
42
|
pace === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
|
|
39
43
|
: lead < 0 ? t('usageMeterPaceBehind', { points: -lead }) : t('usageMeterPaceEven'),
|
|
40
44
|
quota.resetsAt !== undefined && quota.resetsAt > current ? t('usageMeterReset', {
|
|
@@ -48,8 +52,8 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
48
52
|
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
49
53
|
...style, position: 'relative',
|
|
50
54
|
}, children: [_jsx("div", { style: { height: '100%', borderRadius: 'inherit', width: `${used}%`,
|
|
51
|
-
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }),
|
|
52
|
-
position: 'absolute', left: `clamp(0px, calc(${
|
|
55
|
+
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }), marker !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
|
|
56
|
+
position: 'absolute', left: `clamp(0px, calc(${marker}% - 1px), calc(100% - 2px))`,
|
|
53
57
|
top: 0, bottom: 0, width: 2, background: 'var(--dsw-alias-label-primary)',
|
|
54
58
|
boxShadow: '0 0 0 1px var(--dsw-alias-bg-layer-1)',
|
|
55
59
|
} })] });
|