dsh-oauth-login 0.2.4
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/INSTALL.md +59 -0
- package/INSTALL.zh.md +45 -0
- package/LICENSE +201 -0
- package/README.en.md +104 -0
- package/README.md +104 -0
- package/README.zh.md +3 -0
- package/SECURITY.md +33 -0
- package/cordis.patch.yml +5 -0
- package/docs/network-proxy.md +45 -0
- package/docs/openrouter-sync.md +84 -0
- package/docs/screenshots/01-providers-signed-out.png +0 -0
- package/docs/screenshots/03-copilot-signing-in.png +0 -0
- package/docs/screenshots/04-mixed-live-states.png +0 -0
- package/docs/screenshots/05-copilot-sign-in.gif +0 -0
- package/docs/screenshots/06-cli-status-store.png +0 -0
- package/lib/bin.d.ts +5 -0
- package/lib/bin.js +2132 -0
- package/lib/client.js +1915 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +482 -0
- package/lib/index.js +3105 -0
- package/lib/invariant.d.ts +7 -0
- package/lib/invariant.js +8 -0
- package/package.json +180 -0
- package/src/adapter.ts +207 -0
- package/src/auth-routes.ts +654 -0
- package/src/auth.ts +76 -0
- package/src/bin.ts +185 -0
- package/src/catalog.ts +129 -0
- package/src/client/OpenRouterSyncStatus.tsx +66 -0
- package/src/client/PiLoginSettings.tsx +470 -0
- package/src/client/ProxySettings.tsx +123 -0
- package/src/client/SearchableModelSelect.tsx +595 -0
- package/src/client/draft-input.ts +12 -0
- package/src/client/index.tsx +71 -0
- package/src/client/locales.ts +176 -0
- package/src/client/login-input-copy.ts +34 -0
- package/src/client/login-window.ts +24 -0
- package/src/client/model-filter.ts +45 -0
- package/src/client/openrouter-store.ts +122 -0
- package/src/client/proxy-draft.ts +57 -0
- package/src/extra-models.ts +201 -0
- package/src/hosted-capture.ts +41 -0
- package/src/hosted-images.ts +154 -0
- package/src/http.ts +241 -0
- package/src/ids.ts +17 -0
- package/src/index.ts +183 -0
- package/src/invariant.ts +12 -0
- package/src/model-error-hint.ts +98 -0
- package/src/native-tools.ts +442 -0
- package/src/oauth-refresh.ts +49 -0
- package/src/openrouter-catalog.ts +245 -0
- package/src/openrouter-free.ts +76 -0
- package/src/openrouter-models.ts +155 -0
- package/src/openrouter-types.ts +40 -0
- package/src/plugin-config.ts +30 -0
- package/src/provider.ts +137 -0
- package/src/proxy-config.ts +61 -0
- package/src/proxy-store.ts +45 -0
- package/src/proxy-transport.ts +220 -0
- package/src/redact.ts +22 -0
- package/src/responses-tap.ts +97 -0
- package/src/session.ts +104 -0
- package/src/store.ts +234 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1915 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-oauth-login",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/draft-input.ts
|
|
10
|
+
function applyDraftChange(providerId, event, setDrafts) {
|
|
11
|
+
const value = event.currentTarget.value;
|
|
12
|
+
setDrafts((current) => ({
|
|
13
|
+
...current,
|
|
14
|
+
[providerId]: value
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/client/OpenRouterSyncStatus.tsx
|
|
19
|
+
const CSS = [
|
|
20
|
+
".dsh-or-sync{display:flex;flex-direction:column;gap:4px;padding:6px 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);min-width:0}",
|
|
21
|
+
".dsh-or-sync-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px 8px}",
|
|
22
|
+
".dsh-or-sync-copy{min-width:0;overflow-wrap:anywhere}",
|
|
23
|
+
".dsh-or-sync time{font-variant-numeric:tabular-nums}",
|
|
24
|
+
".dsh-or-refresh{min-height:28px;padding:3px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer}",
|
|
25
|
+
".dsh-or-refresh:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
26
|
+
".dsh-or-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}",
|
|
27
|
+
".dsh-or-refresh:disabled{opacity:.55;cursor:default}",
|
|
28
|
+
".dsh-or-sync-error{color:color-mix(in srgb,var(--dsw-alias-state-warn-label) 50%,var(--dsw-alias-label-primary))}"
|
|
29
|
+
].join("\n");
|
|
30
|
+
function OpenRouterSyncStatus({ catalog, ts, details = false }) {
|
|
31
|
+
const state = (0, react.useSyncExternalStore)(catalog.subscribe, catalog.getSnapshot);
|
|
32
|
+
const [now, setNow] = (0, react.useState)(Date.now);
|
|
33
|
+
const data = state.data;
|
|
34
|
+
const retryAt = data?.retryAt ?? 0;
|
|
35
|
+
(0, react.useEffect)(() => {
|
|
36
|
+
setNow(Date.now());
|
|
37
|
+
if (retryAt <= Date.now()) return;
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
setNow(Date.now());
|
|
40
|
+
}, retryAt - Date.now() + 20);
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
};
|
|
44
|
+
}, [retryAt]);
|
|
45
|
+
const cooling = retryAt > now;
|
|
46
|
+
const working = state.loading || data?.refreshing === true;
|
|
47
|
+
const unavailable = state.error !== null || data?.error !== null && data?.error !== void 0;
|
|
48
|
+
const timestamp = data?.lastUpdatedAt;
|
|
49
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
50
|
+
className: "dsh-or-sync",
|
|
51
|
+
"data-ud-check": details ? "openrouter-settings-sync" : "openrouter-menu-sync",
|
|
52
|
+
"data-ud-min-gap": "4",
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: CSS }),
|
|
55
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
56
|
+
className: "dsh-or-sync-row",
|
|
57
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
58
|
+
className: "dsh-or-sync-copy",
|
|
59
|
+
role: "status",
|
|
60
|
+
"aria-live": "polite",
|
|
61
|
+
children: working ? ts("syncing") : data?.connected ? ts("syncSchedule") : ts("syncSignIn")
|
|
62
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
63
|
+
type: "button",
|
|
64
|
+
className: "dsh-or-refresh",
|
|
65
|
+
disabled: working || cooling || data?.connected !== true,
|
|
66
|
+
onClick: () => {
|
|
67
|
+
catalog.load(true);
|
|
68
|
+
},
|
|
69
|
+
children: working ? ts("syncing") : cooling ? ts("syncCooldown") : ts("syncRefresh")
|
|
70
|
+
})]
|
|
71
|
+
}),
|
|
72
|
+
timestamp != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
73
|
+
className: "dsh-or-sync-copy",
|
|
74
|
+
children: [
|
|
75
|
+
ts("syncUpdated"),
|
|
76
|
+
" ",
|
|
77
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
|
|
78
|
+
dateTime: new Date(timestamp).toISOString(),
|
|
79
|
+
children: new Date(timestamp).toLocaleString()
|
|
80
|
+
}),
|
|
81
|
+
data?.stale ? " · " + ts("syncStale") : ""
|
|
82
|
+
]
|
|
83
|
+
}),
|
|
84
|
+
timestamp == null && data?.connected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ts("syncBuiltin") }),
|
|
85
|
+
unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
86
|
+
role: "status",
|
|
87
|
+
className: "dsh-or-sync-copy dsh-or-sync-error",
|
|
88
|
+
children: ts(state.error === "restart" ? "syncRestart" : data?.error === "save" ? "syncSaveError" : "syncError")
|
|
89
|
+
}),
|
|
90
|
+
details && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ts("freeHelp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ts("forcedPluginWarning") })] })
|
|
91
|
+
]
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/proxy-config.ts
|
|
96
|
+
/** Shared, credential-free settings for this plugin's two outbound transports. */
|
|
97
|
+
const PROXY_SETTINGS_PATH = "/plugins/dsh-oauth-login/network/settings";
|
|
98
|
+
function object(value, keys, label) {
|
|
99
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) throw new Error(`${label}: invalid settings object`);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function channel(value, label) {
|
|
103
|
+
const raw = object(value, ["enabled", "url"], label);
|
|
104
|
+
if (typeof raw.enabled !== "boolean" || typeof raw.url !== "string" || raw.url.length > 1024) throw new Error(`${label}: expected enabled and proxy URL`);
|
|
105
|
+
const text = raw.url.trim();
|
|
106
|
+
if (text === "") return {
|
|
107
|
+
enabled: raw.enabled,
|
|
108
|
+
url: ""
|
|
109
|
+
};
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = new URL(text);
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(`${label}: enter a valid HTTP proxy address and port`);
|
|
115
|
+
}
|
|
116
|
+
if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash || parsed.pathname !== "/" || parsed.port === "0") throw new Error(`${label}: use an HTTP(S) proxy without credentials, path, query or fragment`);
|
|
117
|
+
return {
|
|
118
|
+
enabled: raw.enabled,
|
|
119
|
+
url: parsed.origin
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseProxySettings(value) {
|
|
123
|
+
const raw = object(value, [
|
|
124
|
+
"revision",
|
|
125
|
+
"http",
|
|
126
|
+
"websocket"
|
|
127
|
+
], "Network");
|
|
128
|
+
if (typeof raw.revision !== "number" || !Number.isSafeInteger(raw.revision) || raw.revision < 0) throw new Error("Network: invalid revision");
|
|
129
|
+
return {
|
|
130
|
+
revision: raw.revision,
|
|
131
|
+
http: channel(raw.http, "HTTP"),
|
|
132
|
+
websocket: channel(raw.websocket, "WebSocket")
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/client/proxy-draft.ts
|
|
137
|
+
const DEFAULT_PROXY_ADDRESS = "http://127.0.0.1";
|
|
138
|
+
function isAutomaticProxyDraft({ address, port }) {
|
|
139
|
+
const host = address.trim().replace(/\/$/, "");
|
|
140
|
+
return port.trim() === "" && (host === "" || host === "http://127.0.0.1" || host === "127.0.0.1");
|
|
141
|
+
}
|
|
142
|
+
function proxyDraft(settings) {
|
|
143
|
+
const channel = ({ enabled, url }) => {
|
|
144
|
+
if (url === "") return {
|
|
145
|
+
enabled,
|
|
146
|
+
address: DEFAULT_PROXY_ADDRESS,
|
|
147
|
+
port: ""
|
|
148
|
+
};
|
|
149
|
+
const parsed = new URL(url);
|
|
150
|
+
return {
|
|
151
|
+
enabled,
|
|
152
|
+
address: `${parsed.protocol}//${parsed.hostname}`,
|
|
153
|
+
port: parsed.port || (parsed.protocol === "https:" ? "443" : "80")
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
http: channel(settings.http),
|
|
158
|
+
websocket: channel(settings.websocket)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function proxyDraftSettings(draft, revision) {
|
|
162
|
+
const channel = (value) => {
|
|
163
|
+
const { enabled, address, port } = value;
|
|
164
|
+
const host = address.trim();
|
|
165
|
+
const portText = port.trim();
|
|
166
|
+
if (isAutomaticProxyDraft(value)) return {
|
|
167
|
+
enabled,
|
|
168
|
+
url: ""
|
|
169
|
+
};
|
|
170
|
+
if (host === "" || portText !== "" && (!/^\d{1,5}$/.test(portText) || Number(portText) < 1 || Number(portText) > 65535)) throw new Error("Enter a proxy address and a port from 1 to 65535");
|
|
171
|
+
const parsed = new URL(host.includes("://") ? host : `http://${host}`);
|
|
172
|
+
const pastedPort = /:(\d+)\/?$/.exec(host)?.[1] ?? "";
|
|
173
|
+
if (portText === "" && pastedPort === "") throw new Error("Enter a proxy port from 1 to 65535");
|
|
174
|
+
if (portText !== "") {
|
|
175
|
+
if (pastedPort !== "" && Number(pastedPort) !== Number(portText)) throw new Error("The address and port fields specify different ports");
|
|
176
|
+
parsed.port = portText;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
enabled,
|
|
180
|
+
url: parsed.href
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
return parseProxySettings({
|
|
184
|
+
revision,
|
|
185
|
+
http: channel(draft.http),
|
|
186
|
+
websocket: channel(draft.websocket)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/client/ProxySettings.tsx
|
|
191
|
+
/** Independent of account loading, so a broken network never hides its settings. */
|
|
192
|
+
function ProxySettings({ t }) {
|
|
193
|
+
const id = (0, react.useId)();
|
|
194
|
+
const [settings, setSettings] = (0, react.useState)();
|
|
195
|
+
const [draft, setDraft] = (0, react.useState)();
|
|
196
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
197
|
+
const [error, setError] = (0, react.useState)();
|
|
198
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
199
|
+
const load = (0, react.useCallback)(async (signal) => {
|
|
200
|
+
setBusy(true);
|
|
201
|
+
setError(void 0);
|
|
202
|
+
setSaved(false);
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(PROXY_SETTINGS_PATH, {
|
|
205
|
+
credentials: "same-origin",
|
|
206
|
+
signal
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) throw new Error("Settings unavailable");
|
|
209
|
+
const value = parseProxySettings(await response.json());
|
|
210
|
+
if (signal?.aborted) return;
|
|
211
|
+
setSettings(value);
|
|
212
|
+
setDraft(proxyDraft(value));
|
|
213
|
+
} catch {
|
|
214
|
+
if (!signal?.aborted) setError("proxyLoadFailed");
|
|
215
|
+
} finally {
|
|
216
|
+
if (!signal?.aborted) setBusy(false);
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
(0, react.useEffect)(() => {
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
load(controller.signal);
|
|
222
|
+
return () => {
|
|
223
|
+
controller.abort();
|
|
224
|
+
};
|
|
225
|
+
}, [load]);
|
|
226
|
+
const change = (channel, patch) => {
|
|
227
|
+
setDraft((current) => current === void 0 ? current : {
|
|
228
|
+
...current,
|
|
229
|
+
[channel]: {
|
|
230
|
+
...current[channel],
|
|
231
|
+
...patch
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
setError(void 0);
|
|
235
|
+
setSaved(false);
|
|
236
|
+
};
|
|
237
|
+
const save = async () => {
|
|
238
|
+
if (draft === void 0 || settings === void 0) return;
|
|
239
|
+
let value;
|
|
240
|
+
try {
|
|
241
|
+
value = proxyDraftSettings(draft, settings.revision);
|
|
242
|
+
} catch {
|
|
243
|
+
setError("proxyInvalid");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setBusy(true);
|
|
247
|
+
setError(void 0);
|
|
248
|
+
setSaved(false);
|
|
249
|
+
try {
|
|
250
|
+
const response = await fetch(PROXY_SETTINGS_PATH, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
credentials: "same-origin",
|
|
253
|
+
headers: { "content-type": "application/json" },
|
|
254
|
+
body: JSON.stringify(value)
|
|
255
|
+
});
|
|
256
|
+
if (response.status === 409) {
|
|
257
|
+
setError("proxyConflict");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!response.ok) throw new Error("Settings save failed");
|
|
261
|
+
const updated = parseProxySettings(await response.json());
|
|
262
|
+
setSettings(updated);
|
|
263
|
+
setDraft(proxyDraft(updated));
|
|
264
|
+
setSaved(true);
|
|
265
|
+
} catch {
|
|
266
|
+
setError("proxySaveFailed");
|
|
267
|
+
} finally {
|
|
268
|
+
setBusy(false);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const dirty = settings !== void 0 && draft !== void 0 && JSON.stringify(draft) !== JSON.stringify(proxyDraft(settings));
|
|
272
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
273
|
+
className: "dsh-pi-login-card dsh-oauth-network",
|
|
274
|
+
"aria-labelledby": `${id}-title`,
|
|
275
|
+
children: [
|
|
276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
277
|
+
id: `${id}-title`,
|
|
278
|
+
className: "dsh-pi-login-name",
|
|
279
|
+
children: t("proxyTitle")
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
282
|
+
className: "dsh-pi-login-body",
|
|
283
|
+
children: t("proxyScope")
|
|
284
|
+
}),
|
|
285
|
+
draft === void 0 && busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
286
|
+
className: "dsh-pi-login-body",
|
|
287
|
+
children: t("proxyLoading")
|
|
288
|
+
}) : null,
|
|
289
|
+
draft === void 0 ? null : ["http", "websocket"].map((channel) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
290
|
+
className: "dsh-oauth-proxy-channel",
|
|
291
|
+
disabled: busy,
|
|
292
|
+
children: [
|
|
293
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
|
|
294
|
+
className: "dsh-pi-login-name",
|
|
295
|
+
children: channel === "http" ? "HTTP / HTTPS" : "WebSocket (WS / WSS)"
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
298
|
+
className: "dsh-oauth-proxy-switch",
|
|
299
|
+
htmlFor: `${id}-${channel}-enabled`,
|
|
300
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
301
|
+
id: `${id}-${channel}-enabled`,
|
|
302
|
+
type: "checkbox",
|
|
303
|
+
role: "switch",
|
|
304
|
+
checked: draft[channel].enabled,
|
|
305
|
+
onChange: (event) => change(channel, { enabled: event.currentTarget.checked })
|
|
306
|
+
}), t(channel === "http" ? "proxyHttpEnabled" : "proxyWsEnabled")]
|
|
307
|
+
}),
|
|
308
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
309
|
+
className: "dsh-pi-login-body",
|
|
310
|
+
children: t(channel === "http" ? "proxyHttpHelp" : "proxyWsHelp")
|
|
311
|
+
}),
|
|
312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
313
|
+
className: "dsh-oauth-proxy-fields",
|
|
314
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
315
|
+
htmlFor: `${id}-${channel}-address`,
|
|
316
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("proxyAddress") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
317
|
+
className: "dsh-pi-login-input",
|
|
318
|
+
id: `${id}-${channel}-address`,
|
|
319
|
+
autoComplete: "off",
|
|
320
|
+
spellCheck: false,
|
|
321
|
+
placeholder: DEFAULT_PROXY_ADDRESS,
|
|
322
|
+
value: draft[channel].address,
|
|
323
|
+
disabled: !draft[channel].enabled,
|
|
324
|
+
onChange: (event) => change(channel, { address: event.currentTarget.value })
|
|
325
|
+
})]
|
|
326
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
327
|
+
htmlFor: `${id}-${channel}-port`,
|
|
328
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("proxyPort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
329
|
+
className: "dsh-pi-login-input",
|
|
330
|
+
id: `${id}-${channel}-port`,
|
|
331
|
+
autoComplete: "off",
|
|
332
|
+
inputMode: "numeric",
|
|
333
|
+
placeholder: t("proxyPortPlaceholder"),
|
|
334
|
+
value: draft[channel].port,
|
|
335
|
+
disabled: !draft[channel].enabled,
|
|
336
|
+
onChange: (event) => change(channel, { port: event.currentTarget.value })
|
|
337
|
+
})]
|
|
338
|
+
})]
|
|
339
|
+
}),
|
|
340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
341
|
+
className: "dsh-pi-login-body",
|
|
342
|
+
children: t(!draft[channel].enabled ? "proxyDirect" : isAutomaticProxyDraft(draft[channel]) ? "proxyAuto" : "proxyExplicit")
|
|
343
|
+
})
|
|
344
|
+
]
|
|
345
|
+
}, channel)),
|
|
346
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
347
|
+
className: "dsh-pi-login-body",
|
|
348
|
+
children: t("proxyApplyHelp")
|
|
349
|
+
}),
|
|
350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
351
|
+
"aria-live": "polite",
|
|
352
|
+
children: [error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
353
|
+
className: "dsh-pi-login-error",
|
|
354
|
+
role: "alert",
|
|
355
|
+
children: t(error)
|
|
356
|
+
}), saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
357
|
+
className: "dsh-pi-login-body",
|
|
358
|
+
children: t("proxySaved")
|
|
359
|
+
}) : null]
|
|
360
|
+
}),
|
|
361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
362
|
+
className: "dsh-pi-login-row dsh-oauth-proxy-actions",
|
|
363
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
364
|
+
type: "button",
|
|
365
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-secondary",
|
|
366
|
+
disabled: busy,
|
|
367
|
+
onClick: () => {
|
|
368
|
+
load();
|
|
369
|
+
},
|
|
370
|
+
children: t("proxyReload")
|
|
371
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
372
|
+
type: "button",
|
|
373
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-primary",
|
|
374
|
+
disabled: busy || !dirty,
|
|
375
|
+
onClick: () => {
|
|
376
|
+
save();
|
|
377
|
+
},
|
|
378
|
+
children: t(busy ? "working" : "proxySave")
|
|
379
|
+
})]
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/client/login-input-copy.ts
|
|
386
|
+
const PLAN_COPY = {
|
|
387
|
+
waiting: "waitingForCredential",
|
|
388
|
+
help: "credentialHelp",
|
|
389
|
+
placeholder: "credentialPlaceholder",
|
|
390
|
+
action: "saveCredential",
|
|
391
|
+
required: "credentialRequired"
|
|
392
|
+
};
|
|
393
|
+
const CALLBACK_COPY = {
|
|
394
|
+
waiting: "waitingForCallback",
|
|
395
|
+
help: "callbackHelp",
|
|
396
|
+
placeholder: "callbackPlaceholder",
|
|
397
|
+
action: "submitCallback",
|
|
398
|
+
required: "callbackRequired"
|
|
399
|
+
};
|
|
400
|
+
const AUTH_INPUT_COPY = {
|
|
401
|
+
waiting: "waitingForAuthInput",
|
|
402
|
+
help: "authInputHelp",
|
|
403
|
+
placeholder: "authInputPlaceholder",
|
|
404
|
+
action: "submitAuthInput",
|
|
405
|
+
required: "authInputRequired"
|
|
406
|
+
};
|
|
407
|
+
/** Select visible copy from the provider contract and the actual Pi challenge. */
|
|
408
|
+
function loginInputCopy(authType, challengeType) {
|
|
409
|
+
if (authType === "api_key") return PLAN_COPY;
|
|
410
|
+
if (challengeType === "manual_code") return CALLBACK_COPY;
|
|
411
|
+
return AUTH_INPUT_COPY;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/client/login-window.ts
|
|
415
|
+
/** Deliver a login challenge even when an Electron shell rejects the reserved about:blank popup. */
|
|
416
|
+
function openLoginChallenge(popup, url, openUrl) {
|
|
417
|
+
if (url === void 0) {
|
|
418
|
+
popup?.close();
|
|
419
|
+
return "none";
|
|
420
|
+
}
|
|
421
|
+
if (popup !== null) {
|
|
422
|
+
popup.location.replace(url);
|
|
423
|
+
return "reserved";
|
|
424
|
+
}
|
|
425
|
+
openUrl(url);
|
|
426
|
+
return "fallback";
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/client/PiLoginSettings.tsx
|
|
430
|
+
/** Plugin-owned Pi login page inside the dsh Settings shell. */
|
|
431
|
+
const STATUS_PATH = "/plugins/dsh-oauth-login/auth/status";
|
|
432
|
+
const LOGIN_PATH = "/plugins/dsh-oauth-login/auth/login";
|
|
433
|
+
const CANCEL_PATH = "/plugins/dsh-oauth-login/auth/cancel";
|
|
434
|
+
const COMPLETE_PATH = "/plugins/dsh-oauth-login/auth/complete";
|
|
435
|
+
const LOGOUT_PATH = "/plugins/dsh-oauth-login/auth/logout";
|
|
436
|
+
const POLL_INTERVAL_MS = 1e3;
|
|
437
|
+
const STYLE_ID$1 = "dsh-pi-login-settings-theme";
|
|
438
|
+
/**
|
|
439
|
+
* Theme tokens that track light/dark. Hardcoded white/layer fills break dark mode.
|
|
440
|
+
* Mirrors ModelsSection button vocabulary so OAuth cards match the rest of Settings.
|
|
441
|
+
*/
|
|
442
|
+
const SETTINGS_CSS = `
|
|
443
|
+
.dsh-pi-login-page { display:flex; flex-direction:column; gap:16px; max-width:640px; color:var(--dsw-alias-label-primary); }
|
|
444
|
+
.dsh-pi-login-title { margin:0; font-size:20px; line-height:28px; font-weight:600; color:var(--dsw-alias-label-primary); }
|
|
445
|
+
.dsh-pi-login-body { margin:0; font-size:13px; line-height:20px; color:var(--dsw-alias-label-secondary); }
|
|
446
|
+
.dsh-pi-login-error { margin:0; font-size:13px; line-height:20px; color:var(--dsw-alias-state-error-primary); }
|
|
447
|
+
.dsh-pi-login-stack { display:flex; flex-direction:column; gap:10px; }
|
|
448
|
+
.dsh-pi-login-card {
|
|
449
|
+
display:flex; flex-direction:column; gap:8px; padding:14px 16px;
|
|
450
|
+
border:1px solid var(--dsw-alias-border-l2); border-radius:12px;
|
|
451
|
+
background:var(--dsw-alias-bg-module-platform);
|
|
452
|
+
}
|
|
453
|
+
.dsh-pi-login-row { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; }
|
|
454
|
+
.dsh-pi-login-row-actions { display:flex; align-items:center; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
|
|
455
|
+
.dsh-pi-login-name { margin:0; font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); }
|
|
456
|
+
.dsh-pi-login-status { display:flex; align-items:center; flex-wrap:wrap; gap:6px; font-size:13px; color:var(--dsw-alias-label-secondary); }
|
|
457
|
+
.dsh-pi-login-dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; background:var(--dsw-alias-label-dimmed, #9aa0a6); }
|
|
458
|
+
.dsh-pi-login-dot.is-signed-in { background:var(--dsw-alias-state-success-primary, #22a06b); }
|
|
459
|
+
.dsh-pi-login-dot.is-error { background:var(--dsw-alias-state-error-primary, #d92d20); }
|
|
460
|
+
.dsh-pi-login-dot.is-signing-in { background:var(--dsw-alias-brand-primary, #1677ff); }
|
|
461
|
+
.dsh-pi-login-btn {
|
|
462
|
+
box-sizing:border-box; display:inline-flex; align-items:center; justify-content:center;
|
|
463
|
+
min-height:32px; padding:4px 14px; border-radius:16px; font:inherit; font-size:13px; line-height:20px; cursor:pointer;
|
|
464
|
+
}
|
|
465
|
+
.dsh-pi-login-btn:disabled { opacity:0.55; cursor:not-allowed; }
|
|
466
|
+
.dsh-pi-login-btn-secondary {
|
|
467
|
+
border:1px solid var(--dsw-alias-border-l2);
|
|
468
|
+
background:transparent;
|
|
469
|
+
color:var(--dsw-alias-label-primary);
|
|
470
|
+
}
|
|
471
|
+
.dsh-pi-login-btn-secondary:hover:not(:disabled) {
|
|
472
|
+
background:var(--dsw-alias-interactive-bg-hover);
|
|
473
|
+
}
|
|
474
|
+
.dsh-pi-login-btn-primary {
|
|
475
|
+
border:none;
|
|
476
|
+
background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));
|
|
477
|
+
color:var(--dsw-alias-label-primary-foreground, #fff);
|
|
478
|
+
}
|
|
479
|
+
.dsh-pi-login-btn-primary:hover:not(:disabled) {
|
|
480
|
+
background:var(--dsw-alias-button-primary-hover, var(--dsw-alias-brand-primary));
|
|
481
|
+
}
|
|
482
|
+
.dsh-pi-login-code {
|
|
483
|
+
font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
484
|
+
font-size:18px; letter-spacing:0.08em; font-weight:600; color:var(--dsw-alias-label-primary);
|
|
485
|
+
}
|
|
486
|
+
.dsh-pi-login-link { color:var(--dsw-alias-brand-primary); word-break:break-all; }
|
|
487
|
+
.dsh-pi-login-form { display:flex; flex-direction:column; gap:8px; }
|
|
488
|
+
.dsh-pi-login-input {
|
|
489
|
+
box-sizing:border-box; width:100%; min-height:36px; padding:7px 10px;
|
|
490
|
+
border:1px solid var(--dsw-alias-border-l2); border-radius:8px;
|
|
491
|
+
background:var(--dsw-alias-bg-page-primary, transparent);
|
|
492
|
+
color:var(--dsw-alias-label-primary); font:inherit; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
493
|
+
}
|
|
494
|
+
.dsh-pi-login-input:focus {
|
|
495
|
+
outline:2px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent);
|
|
496
|
+
border-color:var(--dsw-alias-brand-primary);
|
|
497
|
+
}
|
|
498
|
+
.dsh-pi-login-actions { display:flex; justify-content:flex-end; }
|
|
499
|
+
.dsh-oauth-proxy-channel { min-width:0; margin:8px 0 0; padding:12px 0 0; border:0; border-top:1px solid var(--dsw-alias-border-l2); display:flex; flex-direction:column; gap:8px; }
|
|
500
|
+
.dsh-oauth-proxy-channel legend { padding:0 8px 0 0; font-size:13px; }
|
|
501
|
+
.dsh-oauth-proxy-switch { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
|
|
502
|
+
.dsh-oauth-proxy-switch input { width:16px; height:16px; margin:0; accent-color:var(--dsw-alias-brand-primary); }
|
|
503
|
+
.dsh-oauth-proxy-fields { display:grid; grid-template-columns:minmax(0, 1fr) 100px; gap:12px; }
|
|
504
|
+
.dsh-oauth-proxy-fields label { min-width:0; display:flex; flex-direction:column; gap:6px; font-size:12px; }
|
|
505
|
+
.dsh-oauth-proxy-fields input { font-size:13px; }
|
|
506
|
+
.dsh-oauth-proxy-fields input:disabled { opacity:0.55; }
|
|
507
|
+
.dsh-oauth-proxy-actions { justify-content:flex-end; }
|
|
508
|
+
@media (max-width:360px) { .dsh-oauth-proxy-fields { grid-template-columns:minmax(0, 1fr); } }
|
|
509
|
+
`;
|
|
510
|
+
function ensureThemeStyles() {
|
|
511
|
+
if (typeof document === "undefined") return;
|
|
512
|
+
if (document.getElementById(STYLE_ID$1) !== null) return;
|
|
513
|
+
const style = document.createElement("style");
|
|
514
|
+
style.id = STYLE_ID$1;
|
|
515
|
+
style.textContent = SETTINGS_CSS;
|
|
516
|
+
document.head.appendChild(style);
|
|
517
|
+
}
|
|
518
|
+
async function jsonRequest(path, method = "GET", body, signal) {
|
|
519
|
+
const response = await fetch(path, {
|
|
520
|
+
method,
|
|
521
|
+
headers: {
|
|
522
|
+
accept: "application/json",
|
|
523
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
524
|
+
},
|
|
525
|
+
credentials: "same-origin",
|
|
526
|
+
signal,
|
|
527
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
528
|
+
});
|
|
529
|
+
const value = await response.json().catch(() => void 0);
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
const message = typeof value === "object" && value !== null && "error" in value && typeof value.error === "string" ? value.error : `HTTP ${response.status}`;
|
|
532
|
+
throw new Error(message);
|
|
533
|
+
}
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function PiLoginSettings({ t, ts, catalog }) {
|
|
537
|
+
if (t === void 0) throw new Error("Pi login settings requires its translation function");
|
|
538
|
+
const [providers, setProviders] = (0, react.useState)(void 0);
|
|
539
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
540
|
+
const [busy, setBusy] = (0, react.useState)(void 0);
|
|
541
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
542
|
+
const [challengeUrls, setChallengeUrls] = (0, react.useState)({});
|
|
543
|
+
const [cancellable, setCancellable] = (0, react.useState)({});
|
|
544
|
+
const [cancelling, setCancelling] = (0, react.useState)(void 0);
|
|
545
|
+
const operationSequence = (0, react.useRef)(0);
|
|
546
|
+
const statusSequence = (0, react.useRef)(0);
|
|
547
|
+
const operations = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
548
|
+
const startOperation = (provider, canCancel, popup) => {
|
|
549
|
+
const previous = operations.current.get(provider);
|
|
550
|
+
previous?.controller.abort();
|
|
551
|
+
previous?.popup?.close();
|
|
552
|
+
const operation = {
|
|
553
|
+
id: ++operationSequence.current,
|
|
554
|
+
controller: new AbortController(),
|
|
555
|
+
...popup === void 0 ? {} : { popup }
|
|
556
|
+
};
|
|
557
|
+
operations.current.set(provider, operation);
|
|
558
|
+
setBusy(provider);
|
|
559
|
+
setCancellable((current) => ({
|
|
560
|
+
...current,
|
|
561
|
+
[provider]: canCancel
|
|
562
|
+
}));
|
|
563
|
+
return operation;
|
|
564
|
+
};
|
|
565
|
+
const isCurrent = (provider, operation) => operations.current.get(provider)?.id === operation.id;
|
|
566
|
+
const finishOperation = (provider, operation) => {
|
|
567
|
+
if (!isCurrent(provider, operation)) return;
|
|
568
|
+
operations.current.delete(provider);
|
|
569
|
+
setBusy(void 0);
|
|
570
|
+
setCancellable((current) => {
|
|
571
|
+
const { [provider]: _removed, ...next } = current;
|
|
572
|
+
return next;
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
(0, react.useEffect)(() => {
|
|
576
|
+
ensureThemeStyles();
|
|
577
|
+
}, []);
|
|
578
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
579
|
+
const requestId = ++statusSequence.current;
|
|
580
|
+
try {
|
|
581
|
+
const next = await jsonRequest(STATUS_PATH);
|
|
582
|
+
if (requestId !== statusSequence.current) return;
|
|
583
|
+
setProviders(next);
|
|
584
|
+
catalog?.load();
|
|
585
|
+
setError(void 0);
|
|
586
|
+
} catch (caught) {
|
|
587
|
+
if (requestId !== statusSequence.current) return;
|
|
588
|
+
setError(caught instanceof Error ? caught.message : t("requestFailed"));
|
|
589
|
+
}
|
|
590
|
+
}, [t, catalog]);
|
|
591
|
+
(0, react.useEffect)(() => {
|
|
592
|
+
refresh();
|
|
593
|
+
}, [refresh]);
|
|
594
|
+
const signing = providers?.some((provider) => provider.account.status === "signing-in") ?? false;
|
|
595
|
+
(0, react.useEffect)(() => {
|
|
596
|
+
if (!signing) return;
|
|
597
|
+
const timer = window.setInterval(() => {
|
|
598
|
+
refresh();
|
|
599
|
+
}, POLL_INTERVAL_MS);
|
|
600
|
+
return () => {
|
|
601
|
+
window.clearInterval(timer);
|
|
602
|
+
};
|
|
603
|
+
}, [refresh, signing]);
|
|
604
|
+
const signIn = async (id) => {
|
|
605
|
+
const popup = window.open("about:blank", "_blank");
|
|
606
|
+
if (popup !== null) popup.opener = null;
|
|
607
|
+
setChallengeUrls((current) => {
|
|
608
|
+
const { [id]: _removed, ...next } = current;
|
|
609
|
+
return next;
|
|
610
|
+
});
|
|
611
|
+
const operation = startOperation(id, true, popup ?? void 0);
|
|
612
|
+
try {
|
|
613
|
+
const challenge = await jsonRequest(LOGIN_PATH, "POST", { provider: id }, operation.controller.signal);
|
|
614
|
+
if (!isCurrent(id, operation)) return;
|
|
615
|
+
if (challenge.url !== void 0) setChallengeUrls((current) => ({
|
|
616
|
+
...current,
|
|
617
|
+
[id]: challenge.url
|
|
618
|
+
}));
|
|
619
|
+
openLoginChallenge(popup, challenge.url, (url) => {
|
|
620
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
621
|
+
});
|
|
622
|
+
await refresh();
|
|
623
|
+
} catch (caught) {
|
|
624
|
+
popup?.close();
|
|
625
|
+
if (isCurrent(id, operation) && !operation.controller.signal.aborted) setError(caught instanceof Error ? caught.message : t("requestFailed"));
|
|
626
|
+
} finally {
|
|
627
|
+
finishOperation(id, operation);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const submitInput = async (provider) => {
|
|
631
|
+
const value = drafts[provider.id]?.trim() ?? "";
|
|
632
|
+
if (value.length === 0) {
|
|
633
|
+
const challenge = provider.account.status === "signing-in" ? provider.account.input : void 0;
|
|
634
|
+
setError(t(challenge === void 0 ? "authInputRequired" : loginInputCopy(provider.authType, challenge.type).required));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const operation = startOperation(provider.id, true);
|
|
638
|
+
try {
|
|
639
|
+
await jsonRequest(COMPLETE_PATH, "POST", {
|
|
640
|
+
provider: provider.id,
|
|
641
|
+
value
|
|
642
|
+
}, operation.controller.signal);
|
|
643
|
+
if (!isCurrent(provider.id, operation)) return;
|
|
644
|
+
setChallengeUrls((current) => {
|
|
645
|
+
const { [provider.id]: _removed, ...next } = current;
|
|
646
|
+
return next;
|
|
647
|
+
});
|
|
648
|
+
setDrafts((current) => {
|
|
649
|
+
const { [provider.id]: _removed, ...next } = current;
|
|
650
|
+
return next;
|
|
651
|
+
});
|
|
652
|
+
await refresh();
|
|
653
|
+
} catch (caught) {
|
|
654
|
+
if (isCurrent(provider.id, operation) && !operation.controller.signal.aborted) setError(caught instanceof Error ? caught.message : t("requestFailed"));
|
|
655
|
+
} finally {
|
|
656
|
+
finishOperation(provider.id, operation);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
const cancelSignIn = async (id) => {
|
|
660
|
+
const operation = startOperation(id, false);
|
|
661
|
+
setCancelling(id);
|
|
662
|
+
try {
|
|
663
|
+
await jsonRequest(CANCEL_PATH, "POST", { provider: id }, operation.controller.signal);
|
|
664
|
+
if (!isCurrent(id, operation)) return;
|
|
665
|
+
setChallengeUrls((current) => {
|
|
666
|
+
const { [id]: _removed, ...next } = current;
|
|
667
|
+
return next;
|
|
668
|
+
});
|
|
669
|
+
setDrafts((current) => {
|
|
670
|
+
const { [id]: _removed, ...next } = current;
|
|
671
|
+
return next;
|
|
672
|
+
});
|
|
673
|
+
await refresh();
|
|
674
|
+
} catch (caught) {
|
|
675
|
+
if (isCurrent(id, operation) && !operation.controller.signal.aborted) setError(caught instanceof Error ? caught.message : t("requestFailed"));
|
|
676
|
+
} finally {
|
|
677
|
+
if (isCurrent(id, operation)) setCancelling(void 0);
|
|
678
|
+
finishOperation(id, operation);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const reopenSignIn = (url) => {
|
|
682
|
+
openLoginChallenge(null, url, (next) => {
|
|
683
|
+
window.open(next, "_blank", "noopener,noreferrer");
|
|
684
|
+
});
|
|
685
|
+
};
|
|
686
|
+
const signOut = async (id) => {
|
|
687
|
+
setBusy(id);
|
|
688
|
+
try {
|
|
689
|
+
await jsonRequest(LOGOUT_PATH, "POST", { provider: id });
|
|
690
|
+
setChallengeUrls((current) => {
|
|
691
|
+
const { [id]: _removed, ...next } = current;
|
|
692
|
+
return next;
|
|
693
|
+
});
|
|
694
|
+
await refresh();
|
|
695
|
+
} catch (caught) {
|
|
696
|
+
setError(caught instanceof Error ? caught.message : t("requestFailed"));
|
|
697
|
+
} finally {
|
|
698
|
+
setBusy(void 0);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
702
|
+
className: "dsh-pi-login-page",
|
|
703
|
+
"aria-labelledby": "pi-login-settings-title",
|
|
704
|
+
children: [
|
|
705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
706
|
+
id: "pi-login-settings-title",
|
|
707
|
+
className: "dsh-pi-login-title",
|
|
708
|
+
children: t("title")
|
|
709
|
+
}),
|
|
710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxySettings, { t }),
|
|
711
|
+
error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
712
|
+
className: "dsh-pi-login-error",
|
|
713
|
+
children: error
|
|
714
|
+
}) : null,
|
|
715
|
+
providers === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
716
|
+
className: "dsh-pi-login-body",
|
|
717
|
+
children: t("loadingAccount")
|
|
718
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
719
|
+
className: "dsh-pi-login-stack",
|
|
720
|
+
children: providers.map((provider) => {
|
|
721
|
+
const account = provider.account;
|
|
722
|
+
const inputCopy = account.status === "signing-in" && account.input !== void 0 ? loginInputCopy(provider.authType, account.input.type) : void 0;
|
|
723
|
+
const challengeUrl = account.status === "signing-in" ? account.url ?? challengeUrls[provider.id] : void 0;
|
|
724
|
+
const label = account.status === "signed-in" ? t("signedIn") : account.status === "signing-in" ? inputCopy === void 0 ? t("signingIn") : t(inputCopy.waiting) : account.status === "error" ? t("requestFailed") : t("signedOut");
|
|
725
|
+
const dotClass = account.status === "signed-in" ? "dsh-pi-login-dot is-signed-in" : account.status === "error" ? "dsh-pi-login-dot is-error" : account.status === "signing-in" ? "dsh-pi-login-dot is-signing-in" : "dsh-pi-login-dot";
|
|
726
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
727
|
+
className: "dsh-pi-login-card",
|
|
728
|
+
children: [
|
|
729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
730
|
+
className: "dsh-pi-login-row",
|
|
731
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
732
|
+
className: "dsh-pi-login-name",
|
|
733
|
+
children: provider.displayName
|
|
734
|
+
}), account.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
735
|
+
type: "button",
|
|
736
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-secondary",
|
|
737
|
+
disabled: busy !== void 0,
|
|
738
|
+
onClick: () => {
|
|
739
|
+
signOut(provider.id);
|
|
740
|
+
},
|
|
741
|
+
children: busy === provider.id ? t("working") : t("logout")
|
|
742
|
+
}) : account.status === "signing-in" || cancellable[provider.id] === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
743
|
+
className: "dsh-pi-login-row-actions",
|
|
744
|
+
children: [challengeUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
745
|
+
type: "button",
|
|
746
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-primary",
|
|
747
|
+
disabled: busy !== void 0,
|
|
748
|
+
onClick: () => {
|
|
749
|
+
reopenSignIn(challengeUrl);
|
|
750
|
+
},
|
|
751
|
+
children: t("reopenAuthorization")
|
|
752
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
753
|
+
type: "button",
|
|
754
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-secondary",
|
|
755
|
+
disabled: cancelling === provider.id || busy !== void 0 && busy !== provider.id,
|
|
756
|
+
onClick: () => {
|
|
757
|
+
cancelSignIn(provider.id);
|
|
758
|
+
},
|
|
759
|
+
children: cancelling === provider.id ? t("working") : t("cancelLogin")
|
|
760
|
+
})]
|
|
761
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
762
|
+
type: "button",
|
|
763
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-primary",
|
|
764
|
+
disabled: busy !== void 0,
|
|
765
|
+
onClick: () => {
|
|
766
|
+
signIn(provider.id);
|
|
767
|
+
},
|
|
768
|
+
children: busy === provider.id ? t("working") : account.status === "error" ? t("loginAgain") : provider.authType === "api_key" ? t("connectPlan") : t("login")
|
|
769
|
+
})]
|
|
770
|
+
}),
|
|
771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
772
|
+
className: "dsh-pi-login-status",
|
|
773
|
+
role: "status",
|
|
774
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
775
|
+
"aria-hidden": "true",
|
|
776
|
+
className: dotClass
|
|
777
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
778
|
+
}),
|
|
779
|
+
provider.id === "openrouter" && catalog !== void 0 && ts !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenRouterSyncStatus, {
|
|
780
|
+
catalog,
|
|
781
|
+
ts,
|
|
782
|
+
details: true
|
|
783
|
+
}),
|
|
784
|
+
account.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
785
|
+
className: "dsh-pi-login-error",
|
|
786
|
+
children: account.message
|
|
787
|
+
}) : null,
|
|
788
|
+
account.status === "signing-in" && account.userCode !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
789
|
+
className: "dsh-pi-login-body",
|
|
790
|
+
children: [
|
|
791
|
+
t("userCode"),
|
|
792
|
+
" ",
|
|
793
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
794
|
+
className: "dsh-pi-login-code",
|
|
795
|
+
children: account.userCode
|
|
796
|
+
})
|
|
797
|
+
]
|
|
798
|
+
}) : null,
|
|
799
|
+
account.status === "signing-in" && challengeUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
800
|
+
className: "dsh-pi-login-body",
|
|
801
|
+
children: [
|
|
802
|
+
provider.authType === "api_key" && account.input !== void 0 ? t("openPlanPage") : t("openUrl"),
|
|
803
|
+
" ",
|
|
804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
805
|
+
href: challengeUrl,
|
|
806
|
+
target: "_blank",
|
|
807
|
+
rel: "noreferrer",
|
|
808
|
+
className: "dsh-pi-login-link",
|
|
809
|
+
children: challengeUrl
|
|
810
|
+
})
|
|
811
|
+
]
|
|
812
|
+
}) : null,
|
|
813
|
+
account.status === "signing-in" && account.input !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
814
|
+
className: "dsh-pi-login-form",
|
|
815
|
+
onSubmit: (event) => {
|
|
816
|
+
event.preventDefault();
|
|
817
|
+
submitInput(provider);
|
|
818
|
+
},
|
|
819
|
+
children: [
|
|
820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
821
|
+
className: "dsh-pi-login-body",
|
|
822
|
+
children: t(inputCopy?.help ?? "authInputHelp")
|
|
823
|
+
}),
|
|
824
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
825
|
+
type: account.input.type === "secret" ? "password" : "text",
|
|
826
|
+
className: "dsh-pi-login-input",
|
|
827
|
+
"aria-label": account.input.message,
|
|
828
|
+
autoComplete: "off",
|
|
829
|
+
spellCheck: false,
|
|
830
|
+
placeholder: t(inputCopy?.placeholder ?? "authInputPlaceholder"),
|
|
831
|
+
value: drafts[provider.id] ?? "",
|
|
832
|
+
disabled: busy !== void 0,
|
|
833
|
+
onChange: (event) => {
|
|
834
|
+
applyDraftChange(provider.id, event, setDrafts);
|
|
835
|
+
}
|
|
836
|
+
}),
|
|
837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
838
|
+
className: "dsh-pi-login-actions",
|
|
839
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
840
|
+
type: "submit",
|
|
841
|
+
className: "dsh-pi-login-btn dsh-pi-login-btn-primary",
|
|
842
|
+
disabled: busy !== void 0 || (drafts[provider.id]?.trim().length ?? 0) === 0,
|
|
843
|
+
children: busy === provider.id ? t("working") : t(inputCopy?.action ?? "submitAuthInput")
|
|
844
|
+
})
|
|
845
|
+
})
|
|
846
|
+
]
|
|
847
|
+
}) : null
|
|
848
|
+
]
|
|
849
|
+
}, provider.id);
|
|
850
|
+
})
|
|
851
|
+
})
|
|
852
|
+
]
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/client/model-filter.ts
|
|
857
|
+
/**
|
|
858
|
+
* Case-insensitive substring filter over model name, model id, and provider
|
|
859
|
+
* name. Blank queries return the input unchanged (same array identity).
|
|
860
|
+
* Groups whose every model is filtered out disappear; relative order holds.
|
|
861
|
+
* @param groups - provider groups from the shared directory snapshot.
|
|
862
|
+
* @param query - raw user text; trimmed and lowercased here.
|
|
863
|
+
* @returns the filtered groups.
|
|
864
|
+
*/
|
|
865
|
+
function filterGroups(groups, query, freeModels) {
|
|
866
|
+
const q = query.trim().toLowerCase();
|
|
867
|
+
if (q === "" && freeModels === void 0) return groups;
|
|
868
|
+
const out = [];
|
|
869
|
+
for (const group of groups) {
|
|
870
|
+
if (freeModels !== void 0 && group.id !== "pi-openrouter") continue;
|
|
871
|
+
const matches = group.name.toLowerCase().includes(q) ? group.models : group.models.filter((model) => model.name.toLowerCase().includes(q) || model.id.toLowerCase().includes(q));
|
|
872
|
+
const models = freeModels === void 0 ? matches : matches.filter((model) => freeModels.has(model.id));
|
|
873
|
+
if (models.length > 0) out.push({
|
|
874
|
+
...group,
|
|
875
|
+
models
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
return out;
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/openrouter-types.ts
|
|
882
|
+
/** Browser-safe contract. No credentials, provider URLs or server imports. */
|
|
883
|
+
const OPENROUTER_CATALOG_PATH = "/plugins/dsh-oauth-login/openrouter/models";
|
|
884
|
+
const OPENROUTER_REFRESH_PATH = "/plugins/dsh-oauth-login/openrouter/models/refresh";
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/client/SearchableModelSelect.tsx
|
|
887
|
+
/**
|
|
888
|
+
* SearchableModelSelect: plugin-owned shadow of the composer model seat
|
|
889
|
+
* (`conversation.input.model`, registered at priority -1 so it renders over
|
|
890
|
+
* the shipped ModelSelect). Behavior mirrors the official two-level menu —
|
|
891
|
+
* root rows drill into the model list and the effort list over the SAME
|
|
892
|
+
* shared per-session directory — and adds one thing the shipped seat lacks:
|
|
893
|
+
* a local search box on the model pane that filters provider groups by model
|
|
894
|
+
* name/id or provider name while typing. Selection still submits through the
|
|
895
|
+
* injected `select` face, so the host stays the single fact source.
|
|
896
|
+
*/
|
|
897
|
+
const STYLE_ID = "dsh-oauth-model-search-theme";
|
|
898
|
+
/** Theme-token styles mirroring the shipped seat's menu vocabulary. */
|
|
899
|
+
const SEARCH_CSS = `
|
|
900
|
+
.dsh-oauth-ms-root { position:relative; min-width:0; }
|
|
901
|
+
.dsh-oauth-ms-trigger {
|
|
902
|
+
display:flex; align-items:center; gap:4px; min-width:0;
|
|
903
|
+
max-width:min(360px,45cqw); height:28px; padding:0 4px 0 8px;
|
|
904
|
+
border:none; border-radius:24px; outline:none; background:transparent;
|
|
905
|
+
color:var(--dsw-alias-label-secondary); font-size:13px; line-height:20px;
|
|
906
|
+
font-weight:500; cursor:pointer;
|
|
907
|
+
}
|
|
908
|
+
.dsh-oauth-ms-trigger:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
|
|
909
|
+
.dsh-oauth-ms-trigger:focus-visible { box-shadow:0 0 0 2px var(--dsw-alias-border-l3); }
|
|
910
|
+
.dsh-oauth-ms-trigger:disabled { color:var(--dsw-alias-label-dimmed); cursor:default; }
|
|
911
|
+
.dsh-oauth-ms-triggerLabel { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
912
|
+
.dsh-oauth-ms-triggerEffort { flex:0 0 auto; color:var(--dsw-alias-label-caption); }
|
|
913
|
+
.dsh-oauth-ms-chevron { flex:0 0 auto; color:var(--dsw-alias-label-caption); transition:transform 120ms ease; }
|
|
914
|
+
.dsh-oauth-ms-chevronOpen { transform:rotate(180deg); }
|
|
915
|
+
@media (prefers-reduced-motion: reduce) { .dsh-oauth-ms-chevron { transition:none; } }
|
|
916
|
+
.dsh-oauth-ms-menu {
|
|
917
|
+
position:absolute; right:0; bottom:calc(100% + 8px); z-index:20;
|
|
918
|
+
display:flex; flex-direction:column;
|
|
919
|
+
width:max-content; min-width:min(240px,calc(100vw - 32px));
|
|
920
|
+
max-width:min(420px,calc(100vw - 32px));
|
|
921
|
+
max-height:min(360px,calc(100vh - 96px)); overflow:hidden; padding:4px;
|
|
922
|
+
border:1px solid var(--dsw-alias-border-inverted); border-radius:12px;
|
|
923
|
+
background:var(--dsw-specific-menu); box-shadow:var(--dsw-shadow-lv3);
|
|
924
|
+
color:var(--dsw-alias-label-primary);
|
|
925
|
+
--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);
|
|
926
|
+
--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);
|
|
927
|
+
}
|
|
928
|
+
.dsh-oauth-ms-status,.dsh-oauth-ms-empty { padding:10px; color:var(--dsw-alias-label-tertiary); font-size:13px; line-height:20px; }
|
|
929
|
+
.dsh-oauth-ms-errorStrip {
|
|
930
|
+
display:flex; align-items:flex-start; justify-content:space-between; gap:8px;
|
|
931
|
+
margin-bottom:4px; padding:7px 8px; border-radius:8px;
|
|
932
|
+
background:var(--dsw-alias-interactive-bg-hover-danger);
|
|
933
|
+
color:var(--dsw-alias-state-error-primary); font-size:12px; line-height:18px;
|
|
934
|
+
}
|
|
935
|
+
.dsh-oauth-ms-warning {
|
|
936
|
+
display:flex; align-items:flex-start; justify-content:space-between; gap:8px;
|
|
937
|
+
margin-bottom:4px; padding:7px 8px; border-radius:8px;
|
|
938
|
+
background:var(--dsw-alias-bg-module-platform);
|
|
939
|
+
color:var(--dsw-alias-state-warn-label); font-size:12px; line-height:18px;
|
|
940
|
+
}
|
|
941
|
+
.dsh-oauth-ms-retry { flex:0 0 auto; padding:0; border:none; background:transparent; color:inherit; font:inherit; font-weight:600; cursor:pointer; }
|
|
942
|
+
.dsh-oauth-ms-search {
|
|
943
|
+
margin:2px 2px 4px; padding:6px 8px; border:1px solid var(--dsw-alias-border-inverted);
|
|
944
|
+
border-radius:8px; background:transparent; font-size:13px;
|
|
945
|
+
color:var(--dsw-alias-label-primary); outline:none;
|
|
946
|
+
}
|
|
947
|
+
.dsh-oauth-ms-search:focus-visible { box-shadow:0 0 0 2px var(--dsw-alias-border-l3); }
|
|
948
|
+
.dsh-oauth-ms-groups { min-height:0; overflow-y:auto; }
|
|
949
|
+
.dsh-oauth-ms-group + .dsh-oauth-ms-group { margin-top:4px; }
|
|
950
|
+
.dsh-oauth-ms-groupTitle {
|
|
951
|
+
position:sticky; top:0; z-index:1; padding:5px 8px 3px;
|
|
952
|
+
background:var(--dsw-specific-menu); color:var(--dsw-alias-label-tertiary);
|
|
953
|
+
font-size:12px; line-height:18px; font-weight:500;
|
|
954
|
+
}
|
|
955
|
+
.dsh-oauth-ms-option {
|
|
956
|
+
box-sizing:border-box; display:flex; align-items:center; gap:8px;
|
|
957
|
+
width:auto; min-width:100%; min-height:38px; padding:6px 8px;
|
|
958
|
+
border:none; border-radius:10px; outline:none; background:transparent;
|
|
959
|
+
color:inherit; text-align:left; cursor:pointer;
|
|
960
|
+
}
|
|
961
|
+
.dsh-oauth-ms-option:hover:not(:disabled),.dsh-oauth-ms-option:focus-visible { background:var(--dsw-alias-interactive-bg-hover); }
|
|
962
|
+
.dsh-oauth-ms-option:disabled { color:var(--dsw-alias-label-dimmed); cursor:default; }
|
|
963
|
+
.dsh-oauth-ms-optionCopy { display:flex; flex:1; flex-direction:column; min-width:0; }
|
|
964
|
+
.dsh-oauth-ms-modelName { overflow:hidden; color:inherit; font-size:14px; line-height:20px; font-weight:500; text-overflow:ellipsis; white-space:nowrap; }
|
|
965
|
+
.dsh-oauth-ms-description { overflow:hidden; color:var(--dsw-alias-label-tertiary); font-size:12px; line-height:18px; text-overflow:ellipsis; white-space:nowrap; }
|
|
966
|
+
.dsh-oauth-ms-check { display:grid; place-items:center; flex:0 0 18px; color:var(--dsw-alias-label-primary); }
|
|
967
|
+
.dsh-oauth-ms-cell {
|
|
968
|
+
box-sizing:border-box; display:flex; align-items:center; gap:8px;
|
|
969
|
+
width:auto; min-width:100%; height:40px; padding:0 10px;
|
|
970
|
+
border:none; border-radius:10px; background:transparent;
|
|
971
|
+
color:var(--dsw-alias-label-primary); font-size:14px; line-height:22px;
|
|
972
|
+
cursor:pointer; text-align:left;
|
|
973
|
+
}
|
|
974
|
+
.dsh-oauth-ms-cell:hover { background:var(--dsw-alias-interactive-bg-hover); }
|
|
975
|
+
.dsh-oauth-ms-cellLabel { flex:0 0 auto; white-space:nowrap; }
|
|
976
|
+
.dsh-oauth-ms-cellValue { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; color:var(--dsw-alias-label-tertiary); }
|
|
977
|
+
.dsh-oauth-ms-cellChevron { flex:0 0 auto; color:var(--dsw-alias-label-tertiary); }
|
|
978
|
+
.dsh-oauth-ms-filters { display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:2px 4px 4px; }
|
|
979
|
+
.dsh-oauth-ms-filter { min-height:28px;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer; }
|
|
980
|
+
.dsh-oauth-ms-filter[aria-pressed="true"] { background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3); }
|
|
981
|
+
.dsh-oauth-ms-filter:focus-visible { outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px; }
|
|
982
|
+
.dsh-oauth-ms-meta { display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px; }
|
|
983
|
+
.dsh-oauth-ms-free { padding:1px 6px;border-radius:4px;color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 50%,var(--dsw-alias-label-primary));background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent); }
|
|
984
|
+
.dsh-oauth-ms-cached { color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover); }
|
|
985
|
+
.dsh-oauth-ms-ended { color:color-mix(in srgb,var(--dsw-alias-state-warn-label) 50%,var(--dsw-alias-label-primary));white-space:normal; }
|
|
986
|
+
.dsh-oauth-ms-help { padding:4px 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:normal; }
|
|
987
|
+
@media (max-width:480px) { .dsh-oauth-ms-menu { width:calc(100vw - 32px);max-height:min(520px,calc(100dvh - 120px)); } .dsh-oauth-ms-option { min-height:44px; } }
|
|
988
|
+
`;
|
|
989
|
+
function ensureStyles() {
|
|
990
|
+
if (typeof document === "undefined") return;
|
|
991
|
+
const existing = document.getElementById(STYLE_ID);
|
|
992
|
+
if (existing !== null) {
|
|
993
|
+
existing.textContent = SEARCH_CSS;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const style = document.createElement("style");
|
|
997
|
+
style.id = STYLE_ID;
|
|
998
|
+
style.textContent = SEARCH_CSS;
|
|
999
|
+
document.head.appendChild(style);
|
|
1000
|
+
}
|
|
1001
|
+
/** Small inline chevron-down glyph (avoids importing host primitives). */
|
|
1002
|
+
function Chevron({ className }) {
|
|
1003
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1004
|
+
className,
|
|
1005
|
+
width: "14",
|
|
1006
|
+
height: "14",
|
|
1007
|
+
viewBox: "0 0 16 16",
|
|
1008
|
+
fill: "none",
|
|
1009
|
+
"aria-hidden": "true",
|
|
1010
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1011
|
+
d: "M4 6l4 4 4-4",
|
|
1012
|
+
stroke: "currentColor",
|
|
1013
|
+
strokeWidth: "1.5",
|
|
1014
|
+
strokeLinecap: "round",
|
|
1015
|
+
strokeLinejoin: "round"
|
|
1016
|
+
})
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
/** Small inline chevron-right glyph. */
|
|
1020
|
+
function ChevronRight({ className }) {
|
|
1021
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1022
|
+
className,
|
|
1023
|
+
width: "14",
|
|
1024
|
+
height: "14",
|
|
1025
|
+
viewBox: "0 0 16 16",
|
|
1026
|
+
fill: "none",
|
|
1027
|
+
"aria-hidden": "true",
|
|
1028
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1029
|
+
d: "M6 4l4 4-4 4",
|
|
1030
|
+
stroke: "currentColor",
|
|
1031
|
+
strokeWidth: "1.5",
|
|
1032
|
+
strokeLinecap: "round",
|
|
1033
|
+
strokeLinejoin: "round"
|
|
1034
|
+
})
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Render the searchable composer model seat.
|
|
1039
|
+
* @param props - see {@link SearchableModelSelectProps}.
|
|
1040
|
+
* @returns the trigger and, while open, the search-enabled two-level menu.
|
|
1041
|
+
*/
|
|
1042
|
+
function SearchableModelSelect({ locked, available, directory, catalog, load, select, t, ts }) {
|
|
1043
|
+
const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
|
|
1044
|
+
const catalogState = (0, react.useSyncExternalStore)(catalog.subscribe, catalog.getSnapshot);
|
|
1045
|
+
const metadata = (0, react.useMemo)(() => new Map(catalogState.data?.models.map((model) => [model.id, model]) ?? []), [catalogState.data]);
|
|
1046
|
+
const freeModels = (0, react.useMemo)(() => new Set([...metadata.values()].filter((model) => model.priceStatus === "free").map((model) => model.id)), [metadata]);
|
|
1047
|
+
const [freeOnly, setFreeOnly] = (0, react.useState)(false);
|
|
1048
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1049
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
1050
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
1051
|
+
const lastActionRef = (0, react.useRef)("load");
|
|
1052
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
1053
|
+
const noticeSeq = (0, react.useRef)(0);
|
|
1054
|
+
const noticeTimer = (0, react.useRef)(void 0);
|
|
1055
|
+
const rootRef = (0, react.useRef)(null);
|
|
1056
|
+
const triggerRef = (0, react.useRef)(null);
|
|
1057
|
+
const searchRef = (0, react.useRef)(null);
|
|
1058
|
+
const itemRefs = (0, react.useRef)([]);
|
|
1059
|
+
const id = (0, react.useId)();
|
|
1060
|
+
(0, react.useEffect)(() => {
|
|
1061
|
+
ensureStyles();
|
|
1062
|
+
}, []);
|
|
1063
|
+
(0, react.useEffect)(() => {
|
|
1064
|
+
if (open && pane === "model") searchRef.current?.focus();
|
|
1065
|
+
}, [open, pane]);
|
|
1066
|
+
(0, react.useEffect)(() => {
|
|
1067
|
+
if (catalogState.data?.connected === false) setFreeOnly(false);
|
|
1068
|
+
}, [catalogState.data?.connected]);
|
|
1069
|
+
(0, react.useEffect)(() => {
|
|
1070
|
+
if (available && catalogState.data?.lastUpdatedAt != null) load();
|
|
1071
|
+
}, [
|
|
1072
|
+
available,
|
|
1073
|
+
catalogState.data?.lastUpdatedAt,
|
|
1074
|
+
load
|
|
1075
|
+
]);
|
|
1076
|
+
(0, react.useEffect)(() => () => {
|
|
1077
|
+
window.clearTimeout(noticeTimer.current);
|
|
1078
|
+
}, []);
|
|
1079
|
+
const choices = (0, react.useMemo)(() => state.groups.flatMap((group) => group.models.map((model) => ({
|
|
1080
|
+
group,
|
|
1081
|
+
model,
|
|
1082
|
+
selection: {
|
|
1083
|
+
provider: group.id,
|
|
1084
|
+
model: model.id,
|
|
1085
|
+
...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
|
|
1086
|
+
}
|
|
1087
|
+
}))), [state.groups]);
|
|
1088
|
+
const current = state.current;
|
|
1089
|
+
const currentChoice = choices[current === null ? -1 : choices.findIndex((c) => c.selection.provider === current.provider && c.selection.model === current.model)];
|
|
1090
|
+
const reasoning = currentChoice?.model.reasoning;
|
|
1091
|
+
const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
1092
|
+
const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
1093
|
+
const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
|
|
1094
|
+
key: "provider-default",
|
|
1095
|
+
effort: void 0,
|
|
1096
|
+
label: t("effort.providerDefault")
|
|
1097
|
+
}] : [], ...reasoning.efforts.map((effort) => ({
|
|
1098
|
+
key: `effort:${effort.id}`,
|
|
1099
|
+
effort: effort.id,
|
|
1100
|
+
label: effort.name,
|
|
1101
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
1102
|
+
}))], [reasoning, t]);
|
|
1103
|
+
const busy = state.status === "selecting";
|
|
1104
|
+
const filteredGroups = (0, react.useMemo)(() => filterGroups(state.groups, query, freeOnly ? freeModels : void 0), [
|
|
1105
|
+
state.groups,
|
|
1106
|
+
query,
|
|
1107
|
+
freeOnly,
|
|
1108
|
+
freeModels
|
|
1109
|
+
]);
|
|
1110
|
+
const filteredCount = (0, react.useMemo)(() => filteredGroups.reduce((sum, group) => sum + group.models.length, 0), [filteredGroups]);
|
|
1111
|
+
const reload = () => {
|
|
1112
|
+
lastActionRef.current = "load";
|
|
1113
|
+
load();
|
|
1114
|
+
catalog.load();
|
|
1115
|
+
};
|
|
1116
|
+
(0, react.useEffect)(() => {
|
|
1117
|
+
if (available) {
|
|
1118
|
+
lastActionRef.current = "load";
|
|
1119
|
+
load();
|
|
1120
|
+
}
|
|
1121
|
+
}, [available, load]);
|
|
1122
|
+
(0, react.useEffect)(() => {
|
|
1123
|
+
if (!open) return;
|
|
1124
|
+
const closeOutside = (event) => {
|
|
1125
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
1126
|
+
};
|
|
1127
|
+
document.addEventListener("mousedown", closeOutside);
|
|
1128
|
+
return () => {
|
|
1129
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
1130
|
+
};
|
|
1131
|
+
}, [open]);
|
|
1132
|
+
if (!available) return null;
|
|
1133
|
+
const show = () => {
|
|
1134
|
+
setPane("root");
|
|
1135
|
+
setQuery("");
|
|
1136
|
+
setNotice(null);
|
|
1137
|
+
setOpen(true);
|
|
1138
|
+
reload();
|
|
1139
|
+
};
|
|
1140
|
+
const close = (restoreFocus = false) => {
|
|
1141
|
+
setOpen(false);
|
|
1142
|
+
setPane("root");
|
|
1143
|
+
setQuery("");
|
|
1144
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
1145
|
+
triggerRef.current?.focus();
|
|
1146
|
+
});
|
|
1147
|
+
};
|
|
1148
|
+
const moveFocus = (offset) => {
|
|
1149
|
+
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
1150
|
+
if (items.length === 0) return;
|
|
1151
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
1152
|
+
items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus();
|
|
1153
|
+
};
|
|
1154
|
+
const onRootKeyDown = (event) => {
|
|
1155
|
+
if (event.key === "Escape" && open) {
|
|
1156
|
+
event.preventDefault();
|
|
1157
|
+
if (pane !== "root") setPane("root");
|
|
1158
|
+
else close(true);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (!open) return;
|
|
1162
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1163
|
+
event.preventDefault();
|
|
1164
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
const onBlur = (event) => {
|
|
1168
|
+
if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
|
|
1169
|
+
close();
|
|
1170
|
+
};
|
|
1171
|
+
const settleSelection = (accepted) => {
|
|
1172
|
+
if (accepted) {
|
|
1173
|
+
if (rootRef.current !== null) close(true);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const message = directory.getSnapshot().error;
|
|
1177
|
+
if (message !== null) {
|
|
1178
|
+
noticeSeq.current += 1;
|
|
1179
|
+
window.clearTimeout(noticeTimer.current);
|
|
1180
|
+
setNotice({
|
|
1181
|
+
seq: noticeSeq.current,
|
|
1182
|
+
text: t("error.action", { message })
|
|
1183
|
+
});
|
|
1184
|
+
noticeTimer.current = window.setTimeout(() => {
|
|
1185
|
+
setNotice(null);
|
|
1186
|
+
}, 4e3);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
const choose = (selection) => {
|
|
1190
|
+
const info = selection.provider === "pi-openrouter" ? metadata.get(selection.model) : void 0;
|
|
1191
|
+
if (info?.freeOnly && info.priceStatus !== "free") return;
|
|
1192
|
+
const current = state.current;
|
|
1193
|
+
if (current?.provider === selection.provider && current.model === selection.model) {
|
|
1194
|
+
close(true);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
lastActionRef.current = "select";
|
|
1198
|
+
select(selection).then(settleSelection);
|
|
1199
|
+
};
|
|
1200
|
+
const chooseEffort = (effort) => {
|
|
1201
|
+
if (state.current === null) return;
|
|
1202
|
+
if (effectiveEffort === effort) {
|
|
1203
|
+
close(true);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const selection = {
|
|
1207
|
+
provider: state.current.provider,
|
|
1208
|
+
model: state.current.model,
|
|
1209
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
1210
|
+
};
|
|
1211
|
+
lastActionRef.current = "select";
|
|
1212
|
+
select(selection).then(settleSelection);
|
|
1213
|
+
};
|
|
1214
|
+
/** Search-input keyboard: ↓ enters the list, Enter picks the first match, Escape clears then backs out. */
|
|
1215
|
+
const onSearchKeyDown = (event) => {
|
|
1216
|
+
if (event.key === "ArrowDown") {
|
|
1217
|
+
event.preventDefault();
|
|
1218
|
+
event.stopPropagation();
|
|
1219
|
+
itemRefs.current.find((item) => item !== null)?.focus();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (event.key === "Enter") {
|
|
1223
|
+
event.preventDefault();
|
|
1224
|
+
const first = filteredGroups[0]?.models[0];
|
|
1225
|
+
if (first !== void 0) choose({
|
|
1226
|
+
provider: filteredGroups[0].id,
|
|
1227
|
+
model: first.id
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (event.key === "Escape") {
|
|
1232
|
+
event.stopPropagation();
|
|
1233
|
+
if (query !== "") setQuery("");
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
const modelLabel = currentChoice?.model.name ?? state.current?.model ?? t("trigger.fallback");
|
|
1237
|
+
const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
|
|
1238
|
+
const triggerAria = currentChoice === void 0 ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
|
|
1239
|
+
model: modelLabel,
|
|
1240
|
+
effort: effortLabel
|
|
1241
|
+
});
|
|
1242
|
+
itemRefs.current = [];
|
|
1243
|
+
let itemIndex = 0;
|
|
1244
|
+
const itemRef = () => {
|
|
1245
|
+
const at = itemIndex++;
|
|
1246
|
+
return (node) => {
|
|
1247
|
+
itemRefs.current[at] = node;
|
|
1248
|
+
};
|
|
1249
|
+
};
|
|
1250
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1251
|
+
ref: rootRef,
|
|
1252
|
+
className: "dsh-oauth-ms-root",
|
|
1253
|
+
onKeyDown: onRootKeyDown,
|
|
1254
|
+
onBlur,
|
|
1255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1256
|
+
ref: triggerRef,
|
|
1257
|
+
type: "button",
|
|
1258
|
+
className: "dsh-oauth-ms-trigger",
|
|
1259
|
+
"aria-label": triggerAria,
|
|
1260
|
+
"aria-haspopup": "menu",
|
|
1261
|
+
"aria-expanded": open,
|
|
1262
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1263
|
+
title: triggerLabel,
|
|
1264
|
+
disabled: locked,
|
|
1265
|
+
onClick: () => {
|
|
1266
|
+
if (open) close();
|
|
1267
|
+
else show();
|
|
1268
|
+
},
|
|
1269
|
+
children: [
|
|
1270
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1271
|
+
className: "dsh-oauth-ms-triggerLabel",
|
|
1272
|
+
children: modelLabel
|
|
1273
|
+
}),
|
|
1274
|
+
effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1275
|
+
className: "dsh-oauth-ms-triggerEffort",
|
|
1276
|
+
children: effortLabel
|
|
1277
|
+
}),
|
|
1278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { className: `dsh-oauth-ms-chevron${open ? " dsh-oauth-ms-chevronOpen" : ""}` })
|
|
1279
|
+
]
|
|
1280
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1281
|
+
id: `${id}-menu`,
|
|
1282
|
+
className: "dsh-oauth-ms-menu",
|
|
1283
|
+
role: "menu",
|
|
1284
|
+
"aria-label": t("menu.aria"),
|
|
1285
|
+
"aria-busy": state.status === "loading" || busy,
|
|
1286
|
+
children: [
|
|
1287
|
+
pane === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1288
|
+
ref: itemRef(),
|
|
1289
|
+
type: "button",
|
|
1290
|
+
role: "menuitem",
|
|
1291
|
+
className: "dsh-oauth-ms-cell",
|
|
1292
|
+
onClick: () => {
|
|
1293
|
+
setPane("model");
|
|
1294
|
+
},
|
|
1295
|
+
children: [
|
|
1296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1297
|
+
className: "dsh-oauth-ms-cellLabel",
|
|
1298
|
+
children: t("menu.model")
|
|
1299
|
+
}),
|
|
1300
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1301
|
+
className: "dsh-oauth-ms-cellValue",
|
|
1302
|
+
children: modelLabel
|
|
1303
|
+
}),
|
|
1304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "dsh-oauth-ms-cellChevron" })
|
|
1305
|
+
]
|
|
1306
|
+
}), reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1307
|
+
ref: itemRef(),
|
|
1308
|
+
type: "button",
|
|
1309
|
+
role: "menuitem",
|
|
1310
|
+
className: "dsh-oauth-ms-cell",
|
|
1311
|
+
onClick: () => {
|
|
1312
|
+
setPane("effort");
|
|
1313
|
+
},
|
|
1314
|
+
children: [
|
|
1315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1316
|
+
className: "dsh-oauth-ms-cellLabel",
|
|
1317
|
+
children: t("menu.effort")
|
|
1318
|
+
}),
|
|
1319
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1320
|
+
className: "dsh-oauth-ms-cellValue",
|
|
1321
|
+
children: effortLabel
|
|
1322
|
+
}),
|
|
1323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "dsh-oauth-ms-cellChevron" })
|
|
1324
|
+
]
|
|
1325
|
+
})] }),
|
|
1326
|
+
pane === "model" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1328
|
+
ref: searchRef,
|
|
1329
|
+
type: "text",
|
|
1330
|
+
className: "dsh-oauth-ms-search",
|
|
1331
|
+
placeholder: ts("placeholder"),
|
|
1332
|
+
"aria-label": ts("aria"),
|
|
1333
|
+
value: query,
|
|
1334
|
+
autoComplete: "off",
|
|
1335
|
+
spellCheck: false,
|
|
1336
|
+
onChange: (event) => {
|
|
1337
|
+
setQuery(event.target.value);
|
|
1338
|
+
},
|
|
1339
|
+
onKeyDown: onSearchKeyDown
|
|
1340
|
+
}),
|
|
1341
|
+
(catalogState.data?.connected || state.groups.some((group) => group.id === "pi-openrouter")) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1342
|
+
className: "dsh-oauth-ms-filters",
|
|
1343
|
+
role: "group",
|
|
1344
|
+
"aria-label": ts("freeFilterAria"),
|
|
1345
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1346
|
+
type: "button",
|
|
1347
|
+
className: "dsh-oauth-ms-filter",
|
|
1348
|
+
"aria-pressed": !freeOnly,
|
|
1349
|
+
onClick: () => {
|
|
1350
|
+
setFreeOnly(false);
|
|
1351
|
+
},
|
|
1352
|
+
children: ts("all")
|
|
1353
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1354
|
+
type: "button",
|
|
1355
|
+
className: "dsh-oauth-ms-filter",
|
|
1356
|
+
"aria-pressed": freeOnly,
|
|
1357
|
+
onClick: () => {
|
|
1358
|
+
setFreeOnly(true);
|
|
1359
|
+
},
|
|
1360
|
+
children: [
|
|
1361
|
+
ts("freeOnly"),
|
|
1362
|
+
" · ",
|
|
1363
|
+
freeModels.size
|
|
1364
|
+
]
|
|
1365
|
+
})]
|
|
1366
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenRouterSyncStatus, {
|
|
1367
|
+
catalog,
|
|
1368
|
+
ts
|
|
1369
|
+
})] }),
|
|
1370
|
+
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1371
|
+
className: "dsh-oauth-ms-status",
|
|
1372
|
+
children: t("status.loading")
|
|
1373
|
+
}),
|
|
1374
|
+
state.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1375
|
+
className: "dsh-oauth-ms-errorStrip",
|
|
1376
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1377
|
+
type: "button",
|
|
1378
|
+
className: "dsh-oauth-ms-retry",
|
|
1379
|
+
onClick: reload,
|
|
1380
|
+
children: t("retry")
|
|
1381
|
+
})]
|
|
1382
|
+
}),
|
|
1383
|
+
state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1384
|
+
className: "dsh-oauth-ms-warning",
|
|
1385
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
|
|
1386
|
+
name: failure.name,
|
|
1387
|
+
message: failure.message
|
|
1388
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1389
|
+
type: "button",
|
|
1390
|
+
className: "dsh-oauth-ms-retry",
|
|
1391
|
+
onClick: reload,
|
|
1392
|
+
children: t("retry")
|
|
1393
|
+
})]
|
|
1394
|
+
}, failure.id)),
|
|
1395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1396
|
+
className: "dsh-oauth-ms-groups",
|
|
1397
|
+
children: filteredGroups.map((group) => {
|
|
1398
|
+
const headingId = `${id}-${group.id}`;
|
|
1399
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1400
|
+
role: "group",
|
|
1401
|
+
"aria-labelledby": headingId,
|
|
1402
|
+
className: "dsh-oauth-ms-group",
|
|
1403
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1404
|
+
className: "dsh-oauth-ms-groupTitle",
|
|
1405
|
+
id: headingId,
|
|
1406
|
+
children: group.name
|
|
1407
|
+
}), group.models.map((model) => {
|
|
1408
|
+
const selected = state.current?.provider === group.id && state.current.model === model.id;
|
|
1409
|
+
const info = group.id === "pi-openrouter" ? metadata.get(model.id) : void 0;
|
|
1410
|
+
const blocked = info?.freeOnly === true && info.priceStatus !== "free";
|
|
1411
|
+
const tokens = info === void 0 ? "" : new Intl.NumberFormat(void 0, {
|
|
1412
|
+
notation: "compact",
|
|
1413
|
+
maximumFractionDigits: 1
|
|
1414
|
+
}).format(info.contextWindow);
|
|
1415
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1416
|
+
ref: itemRef(),
|
|
1417
|
+
type: "button",
|
|
1418
|
+
role: "menuitemradio",
|
|
1419
|
+
"aria-checked": selected,
|
|
1420
|
+
className: "dsh-oauth-ms-option",
|
|
1421
|
+
title: model.name,
|
|
1422
|
+
disabled: busy || blocked,
|
|
1423
|
+
onClick: () => {
|
|
1424
|
+
choose({
|
|
1425
|
+
provider: group.id,
|
|
1426
|
+
model: model.id
|
|
1427
|
+
});
|
|
1428
|
+
},
|
|
1429
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1430
|
+
className: "dsh-oauth-ms-optionCopy",
|
|
1431
|
+
children: [
|
|
1432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1433
|
+
className: "dsh-oauth-ms-modelName",
|
|
1434
|
+
children: model.name
|
|
1435
|
+
}),
|
|
1436
|
+
model.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1437
|
+
className: "dsh-oauth-ms-description",
|
|
1438
|
+
children: model.description
|
|
1439
|
+
}),
|
|
1440
|
+
info !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1441
|
+
className: "dsh-oauth-ms-meta",
|
|
1442
|
+
children: [
|
|
1443
|
+
info.priceStatus === "free" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1444
|
+
className: "dsh-oauth-ms-free" + (catalogState.data?.stale ? " dsh-oauth-ms-cached" : ""),
|
|
1445
|
+
children: ts(catalogState.data?.stale ? "freeCached" : "free")
|
|
1446
|
+
}),
|
|
1447
|
+
blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1448
|
+
className: "dsh-oauth-ms-ended",
|
|
1449
|
+
children: ts(info.priceStatus === "paid" ? "freeEnded" : "freeUnknown")
|
|
1450
|
+
}),
|
|
1451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ts(info.tools ? "tools" : "chatOnly") }),
|
|
1452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ts("context", { tokens }) })
|
|
1453
|
+
]
|
|
1454
|
+
})
|
|
1455
|
+
]
|
|
1456
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1457
|
+
className: "dsh-oauth-ms-check",
|
|
1458
|
+
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1459
|
+
width: "16",
|
|
1460
|
+
height: "16",
|
|
1461
|
+
viewBox: "0 0 16 16",
|
|
1462
|
+
fill: "none",
|
|
1463
|
+
"aria-hidden": "true",
|
|
1464
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1465
|
+
d: "M3 8.5l3.2 3L13 5",
|
|
1466
|
+
stroke: "currentColor",
|
|
1467
|
+
strokeWidth: "1.5",
|
|
1468
|
+
strokeLinecap: "round",
|
|
1469
|
+
strokeLinejoin: "round"
|
|
1470
|
+
})
|
|
1471
|
+
}) : null
|
|
1472
|
+
})]
|
|
1473
|
+
}, model.id);
|
|
1474
|
+
})]
|
|
1475
|
+
}, group.id);
|
|
1476
|
+
})
|
|
1477
|
+
}),
|
|
1478
|
+
state.status === "ready" && state.groups.length > 0 && filteredCount === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1479
|
+
className: "dsh-oauth-ms-empty",
|
|
1480
|
+
children: freeOnly ? ts("emptyFree") : ts("empty", { query: query.trim() })
|
|
1481
|
+
}),
|
|
1482
|
+
state.status === "ready" && state.groups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1483
|
+
className: "dsh-oauth-ms-empty",
|
|
1484
|
+
children: t("empty.models")
|
|
1485
|
+
}),
|
|
1486
|
+
freeOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1487
|
+
className: "dsh-oauth-ms-help",
|
|
1488
|
+
children: ts("freeHelp")
|
|
1489
|
+
})
|
|
1490
|
+
] }),
|
|
1491
|
+
pane === "effort" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1492
|
+
className: "dsh-oauth-ms-errorStrip",
|
|
1493
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1494
|
+
type: "button",
|
|
1495
|
+
className: "dsh-oauth-ms-retry",
|
|
1496
|
+
onClick: reload,
|
|
1497
|
+
children: t("action.reload")
|
|
1498
|
+
})]
|
|
1499
|
+
}), effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1500
|
+
className: "dsh-oauth-ms-empty",
|
|
1501
|
+
children: t("empty.efforts")
|
|
1502
|
+
}) : effortChoices.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1503
|
+
ref: itemRef(),
|
|
1504
|
+
type: "button",
|
|
1505
|
+
role: "menuitemradio",
|
|
1506
|
+
"aria-checked": effectiveEffort === level.effort,
|
|
1507
|
+
className: "dsh-oauth-ms-option",
|
|
1508
|
+
disabled: busy,
|
|
1509
|
+
onClick: () => {
|
|
1510
|
+
chooseEffort(level.effort);
|
|
1511
|
+
},
|
|
1512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1513
|
+
className: "dsh-oauth-ms-optionCopy",
|
|
1514
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1515
|
+
className: "dsh-oauth-ms-modelName",
|
|
1516
|
+
children: level.label
|
|
1517
|
+
}), level.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1518
|
+
className: "dsh-oauth-ms-description",
|
|
1519
|
+
children: level.description
|
|
1520
|
+
})]
|
|
1521
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1522
|
+
className: "dsh-oauth-ms-check",
|
|
1523
|
+
children: effectiveEffort === level.effort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1524
|
+
width: "16",
|
|
1525
|
+
height: "16",
|
|
1526
|
+
viewBox: "0 0 16 16",
|
|
1527
|
+
fill: "none",
|
|
1528
|
+
"aria-hidden": "true",
|
|
1529
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1530
|
+
d: "M3 8.5l3.2 3L13 5",
|
|
1531
|
+
stroke: "currentColor",
|
|
1532
|
+
strokeWidth: "1.5",
|
|
1533
|
+
strokeLinecap: "round",
|
|
1534
|
+
strokeLinejoin: "round"
|
|
1535
|
+
})
|
|
1536
|
+
}) : null
|
|
1537
|
+
})]
|
|
1538
|
+
}, level.key))] }),
|
|
1539
|
+
notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1540
|
+
role: "alert",
|
|
1541
|
+
className: "dsh-oauth-ms-errorStrip",
|
|
1542
|
+
children: notice.text
|
|
1543
|
+
}, notice.seq)
|
|
1544
|
+
]
|
|
1545
|
+
})]
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/client/locales.ts
|
|
1550
|
+
const en = {
|
|
1551
|
+
nav: "Subscription Login",
|
|
1552
|
+
title: "Subscription Login",
|
|
1553
|
+
loadingAccount: "Loading accounts…",
|
|
1554
|
+
signedOut: "Not signed in",
|
|
1555
|
+
signingIn: "Waiting for authorization…",
|
|
1556
|
+
waitingForCredential: "Waiting for a Plan API key…",
|
|
1557
|
+
waitingForCallback: "Waiting for the authorization callback…",
|
|
1558
|
+
waitingForAuthInput: "Waiting for authorization details…",
|
|
1559
|
+
signedIn: "Signed in",
|
|
1560
|
+
login: "Sign in",
|
|
1561
|
+
connectPlan: "Connect plan",
|
|
1562
|
+
loginAgain: "Sign in again",
|
|
1563
|
+
reopenAuthorization: "Reopen authorization",
|
|
1564
|
+
cancelLogin: "Cancel",
|
|
1565
|
+
logout: "Sign out",
|
|
1566
|
+
working: "Working…",
|
|
1567
|
+
userCode: "Code",
|
|
1568
|
+
openUrl: "Authorize",
|
|
1569
|
+
openPlanPage: "Plan page",
|
|
1570
|
+
credentialHelp: "Create or copy the provider-specific Plan API key, then paste it here. It stays in DSH’s private credential file.",
|
|
1571
|
+
credentialPlaceholder: "Paste Plan API key",
|
|
1572
|
+
saveCredential: "Save and connect",
|
|
1573
|
+
credentialRequired: "Paste the Plan API key first.",
|
|
1574
|
+
callbackHelp: "Paste the complete callback URL from the browser after authorization. DSH uses it only to finish this sign-in.",
|
|
1575
|
+
callbackPlaceholder: "Paste the callback URL",
|
|
1576
|
+
submitCallback: "Complete sign-in",
|
|
1577
|
+
callbackRequired: "Paste the callback URL first.",
|
|
1578
|
+
authInputHelp: "Enter the value requested by the provider to continue authorization.",
|
|
1579
|
+
authInputPlaceholder: "Enter authorization value",
|
|
1580
|
+
submitAuthInput: "Continue",
|
|
1581
|
+
authInputRequired: "Enter the requested authorization value first.",
|
|
1582
|
+
requestFailed: "The login request failed.",
|
|
1583
|
+
proxyTitle: "Network proxy",
|
|
1584
|
+
proxyScope: "Only applies to requests made by this subscription plugin. Does not change your system or browser proxy.",
|
|
1585
|
+
proxyLoading: "Loading network settings…",
|
|
1586
|
+
proxyHttpEnabled: "Use a proxy for HTTP",
|
|
1587
|
+
proxyWsEnabled: "Use a proxy for WebSocket",
|
|
1588
|
+
proxyHttpHelp: "Login, token refresh, model requests over HTTPS / SSE, and model catalogs.",
|
|
1589
|
+
proxyWsHelp: "Model WebSocket connections. This switch controls proxy routing; it does not disable WebSocket itself.",
|
|
1590
|
+
proxyAddress: "Proxy address (HTTP / HTTPS)",
|
|
1591
|
+
proxyPort: "Port",
|
|
1592
|
+
proxyPortPlaceholder: "e.g. 45678",
|
|
1593
|
+
proxyDirect: "Off: connect directly, ignoring proxy environment variables. The address is kept for later.",
|
|
1594
|
+
proxyAuto: "Default address with no port: keep automatic environment / system proxy discovery; connect directly if none is found. Port 80 is not assumed.",
|
|
1595
|
+
proxyExplicit: "Use this HTTP(S) CONNECT proxy instead of the automatically detected one. SOCKS-only ports are not supported.",
|
|
1596
|
+
proxyApplyHelp: "Saving applies to new requests without interrupting a running call. Sessions already using SSE stay on SSE. Original retry limits are unchanged.",
|
|
1597
|
+
proxySave: "Save network settings",
|
|
1598
|
+
proxyReload: "Reload settings",
|
|
1599
|
+
proxySaved: "Saved. New requests use these settings.",
|
|
1600
|
+
proxyLoadFailed: "Network settings could not be loaded. Reload, or activate the updated plugin backend.",
|
|
1601
|
+
proxySaveFailed: "Could not confirm that network settings were saved. Reload to check their current state.",
|
|
1602
|
+
proxyInvalid: "Enter an HTTP(S) proxy address without credentials or a path, and a port from 1 to 65535. If both fields specify a port, they must match.",
|
|
1603
|
+
proxyConflict: "Another page changed these settings. Reload before saving again."
|
|
1604
|
+
};
|
|
1605
|
+
const zh = {
|
|
1606
|
+
nav: "订阅登录",
|
|
1607
|
+
title: "订阅登录",
|
|
1608
|
+
loadingAccount: "正在加载账户…",
|
|
1609
|
+
signedOut: "尚未登录",
|
|
1610
|
+
signingIn: "正在等待授权…",
|
|
1611
|
+
waitingForCredential: "等待输入套餐 API Key…",
|
|
1612
|
+
waitingForCallback: "等待输入授权回调地址…",
|
|
1613
|
+
waitingForAuthInput: "等待输入授权信息…",
|
|
1614
|
+
signedIn: "已登录",
|
|
1615
|
+
login: "登录",
|
|
1616
|
+
connectPlan: "连接套餐",
|
|
1617
|
+
loginAgain: "重新登录",
|
|
1618
|
+
reopenAuthorization: "重新打开授权页",
|
|
1619
|
+
cancelLogin: "取消",
|
|
1620
|
+
logout: "退出",
|
|
1621
|
+
working: "处理中…",
|
|
1622
|
+
userCode: "授权码",
|
|
1623
|
+
openUrl: "授权",
|
|
1624
|
+
openPlanPage: "套餐页面",
|
|
1625
|
+
credentialHelp: "请在套餐页新建或复制专用 API Key,再粘贴到这里。密钥只保存在 DSH 的私有凭据文件中。",
|
|
1626
|
+
credentialPlaceholder: "粘贴套餐 API Key",
|
|
1627
|
+
saveCredential: "保存并连接",
|
|
1628
|
+
credentialRequired: "请先粘贴套餐 API Key。",
|
|
1629
|
+
callbackHelp: "请在浏览器完成授权后,复制完整的回调地址并粘贴到这里。DSH 只用它完成本次登录。",
|
|
1630
|
+
callbackPlaceholder: "粘贴授权回调地址",
|
|
1631
|
+
submitCallback: "完成登录",
|
|
1632
|
+
callbackRequired: "请先粘贴授权回调地址。",
|
|
1633
|
+
authInputHelp: "请输入服务商要求的内容以继续授权。",
|
|
1634
|
+
authInputPlaceholder: "输入授权信息",
|
|
1635
|
+
submitAuthInput: "继续",
|
|
1636
|
+
authInputRequired: "请先输入服务商要求的授权信息。",
|
|
1637
|
+
requestFailed: "登录请求失败。",
|
|
1638
|
+
proxyTitle: "网络代理",
|
|
1639
|
+
proxyScope: "仅影响本订阅插件发出的请求,不修改系统或浏览器代理。",
|
|
1640
|
+
proxyLoading: "正在加载网络设置…",
|
|
1641
|
+
proxyHttpEnabled: "HTTP 使用代理",
|
|
1642
|
+
proxyWsEnabled: "WebSocket 使用代理",
|
|
1643
|
+
proxyHttpHelp: "用于登录、令牌刷新、HTTPS / SSE 模型请求和模型列表。",
|
|
1644
|
+
proxyWsHelp: "用于模型的 WebSocket 连接。此开关只控制是否走代理,不关闭 WebSocket 本身。",
|
|
1645
|
+
proxyAddress: "代理地址(HTTP / HTTPS)",
|
|
1646
|
+
proxyPort: "端口",
|
|
1647
|
+
proxyPortPlaceholder: "例如 45678",
|
|
1648
|
+
proxyDirect: "已关闭:直接连接,忽略代理环境变量。保留地址,方便下次开启。",
|
|
1649
|
+
proxyAuto: "默认地址、端口留空:沿用环境变量、系统代理等自动发现;没有找到代理时直连,不会默认使用 80 端口。",
|
|
1650
|
+
proxyExplicit: "优先使用此 HTTP(S) CONNECT 代理,不使用自动发现的地址。不支持纯 SOCKS 端口。",
|
|
1651
|
+
proxyApplyHelp: "保存后用于新请求,不中断正在进行的调用。已降级的会话继续使用 SSE,原有重试上限不变。",
|
|
1652
|
+
proxySave: "保存网络设置",
|
|
1653
|
+
proxyReload: "重新加载",
|
|
1654
|
+
proxySaved: "已保存,新请求将使用这些设置。",
|
|
1655
|
+
proxyLoadFailed: "网络设置加载失败。请重新加载,或先启用更新后的插件服务端。",
|
|
1656
|
+
proxySaveFailed: "未能确认网络设置已保存,请重新加载后检查当前设置。",
|
|
1657
|
+
proxyInvalid: "请填写不含账号密码或路径的 HTTP(S) 代理地址,端口为 1–65535。若地址中也有端口,须与端口栏一致。",
|
|
1658
|
+
proxyConflict: "其他页面已修改设置,请重新加载后再保存。"
|
|
1659
|
+
};
|
|
1660
|
+
/** `model-search` namespace: copy for the searchable composer model seat. */
|
|
1661
|
+
const searchEn = {
|
|
1662
|
+
placeholder: "Search models…",
|
|
1663
|
+
aria: "Filter models",
|
|
1664
|
+
empty: "No models match “{query}”.",
|
|
1665
|
+
all: "All",
|
|
1666
|
+
freeOnly: "Free only",
|
|
1667
|
+
freeFilterAria: "Model price filter (OpenRouter)",
|
|
1668
|
+
free: "Free · Rate limited",
|
|
1669
|
+
freeCached: "Free (cached) · Recheck pending",
|
|
1670
|
+
freeEnded: "Free offer ended · Paid calls blocked",
|
|
1671
|
+
freeUnknown: "Price unconfirmed · Free guard active",
|
|
1672
|
+
tools: "Tool calling",
|
|
1673
|
+
chatOnly: "Chat only",
|
|
1674
|
+
context: "{tokens} context",
|
|
1675
|
+
emptyFree: "No matching free OpenRouter models. Clear the search or refresh the catalog.",
|
|
1676
|
+
syncSchedule: "OpenRouter · Sync every 15 min",
|
|
1677
|
+
syncing: "Syncing models…",
|
|
1678
|
+
syncSignIn: "Sign in to OpenRouter to sync models.",
|
|
1679
|
+
syncRefresh: "Refresh models",
|
|
1680
|
+
syncCooldown: "Refreshed within 60s",
|
|
1681
|
+
syncUpdated: "Last synced",
|
|
1682
|
+
syncStale: "Cached · Refresh needed",
|
|
1683
|
+
syncBuiltin: "Not synced yet; using built-in models.",
|
|
1684
|
+
syncError: "Sync failed. Keeping the previous list; retry later.",
|
|
1685
|
+
syncSaveError: "Could not save the safe catalog cache. Keeping the previous list; check DSH disk permissions.",
|
|
1686
|
+
syncRestart: "Restart DSH to activate the new model-sync backend.",
|
|
1687
|
+
freeHelp: "Free models have rate and daily limits. Unavailable models never fall back to paid ones.",
|
|
1688
|
+
forcedPluginWarning: "Disable account-enforced paid OpenRouter plugins: DSH cannot override platform-enforced charges."
|
|
1689
|
+
};
|
|
1690
|
+
const searchZh = {
|
|
1691
|
+
placeholder: "搜索模型…",
|
|
1692
|
+
aria: "筛选模型",
|
|
1693
|
+
empty: "没有匹配“{query}”的模型。",
|
|
1694
|
+
all: "全部",
|
|
1695
|
+
freeOnly: "仅免费",
|
|
1696
|
+
freeFilterAria: "模型价格筛选(OpenRouter)",
|
|
1697
|
+
free: "免费 · 有限额",
|
|
1698
|
+
freeCached: "免费缓存 · 待更新",
|
|
1699
|
+
freeEnded: "免费已结束 · 付费已拦截",
|
|
1700
|
+
freeUnknown: "价格未确认 · 免费保护中",
|
|
1701
|
+
tools: "支持工具调用",
|
|
1702
|
+
chatOnly: "仅聊天",
|
|
1703
|
+
context: "{tokens} 上下文",
|
|
1704
|
+
emptyFree: "没有匹配的 OpenRouter 免费模型。请清空搜索或刷新列表。",
|
|
1705
|
+
syncSchedule: "OpenRouter · 每 15 分钟同步",
|
|
1706
|
+
syncing: "正在同步模型…",
|
|
1707
|
+
syncSignIn: "登录 OpenRouter 后自动同步模型。",
|
|
1708
|
+
syncRefresh: "刷新模型",
|
|
1709
|
+
syncCooldown: "60 秒内已刷新",
|
|
1710
|
+
syncUpdated: "上次同步",
|
|
1711
|
+
syncStale: "缓存待更新",
|
|
1712
|
+
syncBuiltin: "尚未同步,暂用内置模型列表。",
|
|
1713
|
+
syncError: "同步失败,保留上次列表;请稍后重试。",
|
|
1714
|
+
syncSaveError: "安全缓存保存失败,保留上次列表;请检查 DSH 目录写入权限。",
|
|
1715
|
+
syncRestart: "请重启 DSH,启用新的模型同步服务。",
|
|
1716
|
+
freeHelp: "免费模型有速率和每日限额;不可用时不会切换到付费模型。",
|
|
1717
|
+
forcedPluginWarning: "请关闭 OpenRouter 账户强制启用的收费插件;平台强制规则不能由 DSH 覆盖。"
|
|
1718
|
+
};
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/client/openrouter-store.ts
|
|
1721
|
+
function validSnapshot(value) {
|
|
1722
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1723
|
+
const data = value;
|
|
1724
|
+
const timestamp = (entry) => entry === null || typeof entry === "number" && Number.isFinite(entry) && entry > 0 && entry <= 864e13;
|
|
1725
|
+
if (data.version !== 1 || typeof data.connected !== "boolean" || typeof data.refreshing !== "boolean" || typeof data.stale !== "boolean" || !timestamp(data.lastUpdatedAt) || !timestamp(data.lastAttemptAt) || !timestamp(data.nextRefreshAt) || !timestamp(data.retryAt) || !Array.isArray(data.models) || data.models.length > 1e4) return false;
|
|
1726
|
+
return data.models.every((entry) => {
|
|
1727
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1728
|
+
const model = entry;
|
|
1729
|
+
return typeof model.id === "string" && typeof model.name === "string" && typeof model.tools === "boolean" && typeof model.freeOnly === "boolean" && typeof model.contextWindow === "number" && Number.isFinite(model.contextWindow) && [
|
|
1730
|
+
"free",
|
|
1731
|
+
"paid",
|
|
1732
|
+
"unknown"
|
|
1733
|
+
].includes(String(model.priceStatus));
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
/** One metadata store/poll per browser plugin, shared by settings and menus. */
|
|
1737
|
+
var OpenRouterCatalogClient = class {
|
|
1738
|
+
request;
|
|
1739
|
+
state = {
|
|
1740
|
+
data: null,
|
|
1741
|
+
loading: false,
|
|
1742
|
+
error: null
|
|
1743
|
+
};
|
|
1744
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1745
|
+
pending;
|
|
1746
|
+
controller;
|
|
1747
|
+
poll;
|
|
1748
|
+
lastRead = -Infinity;
|
|
1749
|
+
disposed = false;
|
|
1750
|
+
constructor(request = (...args) => globalThis.fetch(...args)) {
|
|
1751
|
+
this.request = request;
|
|
1752
|
+
}
|
|
1753
|
+
getSnapshot = () => this.state;
|
|
1754
|
+
subscribe = (listener) => {
|
|
1755
|
+
this.listeners.add(listener);
|
|
1756
|
+
if (this.listeners.size === 1 && !this.disposed) this.load();
|
|
1757
|
+
return () => {
|
|
1758
|
+
this.listeners.delete(listener);
|
|
1759
|
+
if (this.listeners.size === 0) {
|
|
1760
|
+
clearTimeout(this.poll);
|
|
1761
|
+
this.poll = void 0;
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
};
|
|
1765
|
+
update(state) {
|
|
1766
|
+
this.state = state;
|
|
1767
|
+
for (const listener of this.listeners) listener();
|
|
1768
|
+
}
|
|
1769
|
+
failed(error) {
|
|
1770
|
+
if (this.disposed) return;
|
|
1771
|
+
this.update({
|
|
1772
|
+
...this.state,
|
|
1773
|
+
error,
|
|
1774
|
+
data: this.state.data === null ? null : {
|
|
1775
|
+
...this.state.data,
|
|
1776
|
+
stale: true
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
load = (force = false) => {
|
|
1781
|
+
if (this.disposed) return Promise.resolve();
|
|
1782
|
+
if (this.pending !== void 0) return this.pending;
|
|
1783
|
+
if (!force && Date.now() - this.lastRead < 1500) {
|
|
1784
|
+
this.schedule();
|
|
1785
|
+
return Promise.resolve();
|
|
1786
|
+
}
|
|
1787
|
+
this.lastRead = Date.now();
|
|
1788
|
+
clearTimeout(this.poll);
|
|
1789
|
+
this.poll = void 0;
|
|
1790
|
+
this.controller = new AbortController();
|
|
1791
|
+
const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(2e4)]);
|
|
1792
|
+
this.update({
|
|
1793
|
+
...this.state,
|
|
1794
|
+
loading: true,
|
|
1795
|
+
error: null
|
|
1796
|
+
});
|
|
1797
|
+
const pending = this.read(force, signal).finally(() => {
|
|
1798
|
+
if (this.pending !== pending) return;
|
|
1799
|
+
this.pending = void 0;
|
|
1800
|
+
this.controller = void 0;
|
|
1801
|
+
if (!this.disposed) {
|
|
1802
|
+
this.update({
|
|
1803
|
+
...this.state,
|
|
1804
|
+
loading: false
|
|
1805
|
+
});
|
|
1806
|
+
this.schedule();
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
this.pending = pending;
|
|
1810
|
+
return pending;
|
|
1811
|
+
};
|
|
1812
|
+
async read(force, signal) {
|
|
1813
|
+
try {
|
|
1814
|
+
const response = await this.request(force ? OPENROUTER_REFRESH_PATH : OPENROUTER_CATALOG_PATH, {
|
|
1815
|
+
method: force ? "POST" : "GET",
|
|
1816
|
+
credentials: "same-origin",
|
|
1817
|
+
headers: { accept: "application/json" },
|
|
1818
|
+
signal
|
|
1819
|
+
});
|
|
1820
|
+
if (response.status === 404) {
|
|
1821
|
+
this.failed("restart");
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
if (!response.ok) throw new Error("catalog unavailable");
|
|
1825
|
+
const data = await response.json();
|
|
1826
|
+
if (!validSnapshot(data)) throw new Error("invalid catalog response");
|
|
1827
|
+
if (!this.disposed) this.update({
|
|
1828
|
+
...this.state,
|
|
1829
|
+
data,
|
|
1830
|
+
error: null
|
|
1831
|
+
});
|
|
1832
|
+
} catch {
|
|
1833
|
+
this.failed("unavailable");
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
schedule() {
|
|
1837
|
+
if (this.disposed || this.listeners.size === 0) return;
|
|
1838
|
+
clearTimeout(this.poll);
|
|
1839
|
+
this.poll = setTimeout(() => {
|
|
1840
|
+
this.load();
|
|
1841
|
+
}, this.state.data?.refreshing ? 2e3 : 6e4);
|
|
1842
|
+
}
|
|
1843
|
+
dispose() {
|
|
1844
|
+
this.disposed = true;
|
|
1845
|
+
clearTimeout(this.poll);
|
|
1846
|
+
this.controller?.abort();
|
|
1847
|
+
this.listeners.clear();
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
//#endregion
|
|
1851
|
+
//#region src/client/index.tsx
|
|
1852
|
+
const name = "dsh-oauth-login-client";
|
|
1853
|
+
const inject = [
|
|
1854
|
+
"slots",
|
|
1855
|
+
"locale",
|
|
1856
|
+
"sessions",
|
|
1857
|
+
"modelDirectories",
|
|
1858
|
+
"remote",
|
|
1859
|
+
"remote.session"
|
|
1860
|
+
];
|
|
1861
|
+
function apply(ctx) {
|
|
1862
|
+
const catalog = new OpenRouterCatalogClient();
|
|
1863
|
+
ctx.effect(() => () => catalog.dispose(), "dsh-oauth-login: catalog metadata");
|
|
1864
|
+
const searchNs = "model-search";
|
|
1865
|
+
ctx.effect(() => ctx.locale.register(searchNs, {
|
|
1866
|
+
zh: searchZh,
|
|
1867
|
+
en: searchEn
|
|
1868
|
+
}), "dsh-oauth-login: model-search copy");
|
|
1869
|
+
const ts = ctx.locale.bind(searchNs);
|
|
1870
|
+
const namespace = "settings.pi-login";
|
|
1871
|
+
ctx.effect(() => ctx.locale.register(namespace, {
|
|
1872
|
+
zh,
|
|
1873
|
+
en
|
|
1874
|
+
}), "dsh-oauth-login: settings copy");
|
|
1875
|
+
const t = ctx.locale.bind(namespace);
|
|
1876
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1877
|
+
name: "settings.section",
|
|
1878
|
+
id: "pi-login",
|
|
1879
|
+
order: 17,
|
|
1880
|
+
label: () => t("nav"),
|
|
1881
|
+
inject: () => ({
|
|
1882
|
+
t,
|
|
1883
|
+
ts,
|
|
1884
|
+
catalog
|
|
1885
|
+
})
|
|
1886
|
+
}, PiLoginSettings));
|
|
1887
|
+
ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
|
|
1888
|
+
name: "conversation.input.model",
|
|
1889
|
+
locale: "model",
|
|
1890
|
+
priority: -1,
|
|
1891
|
+
inject: (sessionId) => {
|
|
1892
|
+
const directory = ctx.modelDirectories.directoryFor(sessionId);
|
|
1893
|
+
const available = ctx.sessions.subagentAddress(sessionId) === void 0;
|
|
1894
|
+
return {
|
|
1895
|
+
available,
|
|
1896
|
+
catalog,
|
|
1897
|
+
directory: directory.store,
|
|
1898
|
+
load: () => {
|
|
1899
|
+
if (available) directory.load().catch(() => {});
|
|
1900
|
+
},
|
|
1901
|
+
select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false),
|
|
1902
|
+
ts
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
}, SearchableModelSelect));
|
|
1906
|
+
}
|
|
1907
|
+
//#endregion
|
|
1908
|
+
exports.apply = apply;
|
|
1909
|
+
exports.inject = inject;
|
|
1910
|
+
exports.name = name;
|
|
1911
|
+
return module.exports;
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
//# sourceMappingURL=client.js.map
|