@router-ai/dsh-plugin-routerai 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1352 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@router-ai/dsh-plugin-routerai",
3
+ factory: function(require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react = require("react");
10
+ // src/links.ts
11
+ const KEYS_PAGE = "https://routerai.ru/settings/keys";
12
+ const BILLING_PAGE = "https://routerai.ru/settings/billing";
13
+ // src/client/logo.ts
14
+ const LOGO = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20fill%3D%22%232d67f6%22%20d%3D%22M8.22%2021.06a4.09%204.09%200%200%200%207.55%200%204.1%204.1%200%200%200%205.34-5.32%204.08%204.08%200%200%200%200-7.53%204.07%204.07%200%200%200-5.34-5.33%204.09%204.09%200%200%200-7.55%200A4.1%204.1%200%200%200%202.9%208.21a4.08%204.08%200%200%200%200%207.53%204.07%204.07%200%200%200%205.33%205.32%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M23.25%2011.97a3.7%203.7%200%200%200-2.28-3.42l-.35-.14.14-.35a3.7%203.7%200%200%200-4.84-4.83l-.35.14-.14-.34A3.7%203.7%200%200%200%2012.29.76L12%20.75c-1.54%200-2.87.94-3.43%202.28l-.14.34-.35-.14a3.72%203.72%200%200%200-4.85%204.83l.15.35-.35.14a3.7%203.7%200%200%200-2.28%203.42v.29a3.7%203.7%200%200%200%202.28%203.13l.35.14-.15.35a3.7%203.7%200%200%200%204.85%204.83l.35-.14.14.35a3.71%203.71%200%200%200%206.86%200l.14-.35.35.14a3.72%203.72%200%200%200%204.85-4.83l-.15-.35.35-.14a3.7%203.7%200%200%200%202.28-3.42M12%201.97c1.03%200%201.91.63%202.29%201.53l.61%201.48%201.49-.61a2.48%202.48%200%200%201%203.23%203.23l-.6%201.47%201.47.62c.9.37%201.53%201.26%201.53%202.28v.09a2.5%202.5%200%200%201-1.53%202.2l-1.48.6.61%201.49a2.47%202.47%200%200%201-3.23%203.23l-1.49-.61-.61%201.47a2.48%202.48%200%200%201-4.58%200l-.61-1.47-1.49.6a2.48%202.48%200%200%201-3.23-3.22l.6-1.48-1.47-.62a2.5%202.5%200%200%201-1.53-2.28v-.09c.04-.99.66-1.83%201.53-2.2l1.48-.6-.61-1.48A2.47%202.47%200%200%201%207.6%204.37l1.49.6.61-1.47A2.5%202.5%200%200%201%2012%201.98m0%2015.8q-.36%200-.66.13l-1.6.66.66%201.6a1.73%201.73%200%200%200%203.2%200l.67-1.6-1.61-.66q-.3-.13-.66-.13m-6.93-1.14a1.72%201.72%200%200%200%202.26%202.25l1.6-.65-.66-1.6a1.7%201.7%200%200%200-.93-.93l-1.61-.67zm11.6-.93a1.7%201.7%200%200%200-.94.93l-.66%201.6%201.6.65a1.74%201.74%200%200%200%202.26-2.25l-.66-1.6zM6.18%2011.97q0-.36-.13-.66l-.66-1.6-1.6.67c-.61.25-1.04.84-1.07%201.52v.08c0%20.7.44%201.32%201.07%201.58l1.6.67.66-1.6q.13-.3.13-.66m8.89%200q0-.71.21-1.36a4.5%204.5%200%200%201-1.93-1.92%204.5%204.5%200%200%201-2.72%200A4.4%204.4%200%200%201%208.7%2010.6a4.4%204.4%200%200%201%200%202.72%204.5%204.5%200%200%201%201.93%201.92%204.5%204.5%200%200%201%202.72%200%204.4%204.4%200%200%201%201.93-1.92%204%204%200%200%201-.21-1.36M7.33%205.06A1.74%201.74%200%200%200%205.07%207.3l.66%201.6%201.6-.66a1.7%201.7%200%200%200%20.94-.93l.66-1.6zm11.22.37c-.5-.5-1.25-.63-1.88-.37l-1.6.66.66%201.6a1.7%201.7%200%200%200%20.93.93l1.61.66.66-1.6c.26-.63.13-1.37-.38-1.88M12%202.73c-.72%200-1.34.43-1.6%201.06l-.67%201.6%201.61.66a1.7%201.7%200%200%200%201.32%200l1.6-.67-.66-1.6A1.7%201.7%200%200%200%2012%202.74m-5.06%209.24q0%20.5-.19.94l-.61%201.48%201.48.62a2.5%202.5%200%200%201%201.34%201.33l.61%201.48%201.49-.61a2.5%202.5%200%200%201%201.88%200l1.49.6.61-1.47A2.5%202.5%200%200%201%2016.38%2015l1.48-.62-.61-1.48a2.5%202.5%200%200%201%200-1.88l.6-1.48-1.47-.61a2.5%202.5%200%200%201-1.34-1.34l-.61-1.47-1.49.6a2.5%202.5%200%200%201-1.88%200l-1.49-.6-.61%201.47a2.5%202.5%200%200%201-1.34%201.34l-1.48.61.61%201.48q.19.43.19.94m8.89%200q0%20.76.27%201.41l.15.35-.35.14a3.7%203.7%200%200%200-2%202l-.14.34-.35-.14a3.7%203.7%200%200%200-2.82%200l-.35.14-.14-.34a3.7%203.7%200%200%200-2-2l-.35-.14.15-.35a3.7%203.7%200%200%200%200-2.82l-.15-.34.35-.15a3.7%203.7%200%200%200%202-2l.14-.34.35.15a3.7%203.7%200%200%200%202.82%200l.35-.15.14.35a3.7%203.7%200%200%200%202%202l.35.14-.15.34q-.26.65-.27%201.41m1.98%200q0%20.36.13.66l.66%201.6%201.6-.67a1.7%201.7%200%200%200%201.07-1.52v-.2a1.7%201.7%200%200%200-1.07-1.46l-1.6-.67-.66%201.6q-.12.3-.13.66m6.19%200c0%201.73-.98%203.22-2.42%203.96a4.44%204.44%200%200%201-5.61%205.6%204.46%204.46%200%200%201-7.94%200%204.47%204.47%200%200%201-5.6-5.6A4.5%204.5%200%200%201%200%2012.14v-.17c0-1.72.98-3.22%202.42-3.96a4.44%204.44%200%200%201%205.61-5.6A4.5%204.5%200%200%201%2012%200h.17a4.5%204.5%200%200%201%203.8%202.41%204.47%204.47%200%200%201%205.6%205.6A4.5%204.5%200%200%201%2024%2011.97%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M10.44%208.22a4%204%200%200%200%203.12%200%204%204%200%200%200%202.2%202.2%204%204%200%200%200%200%203.1%204%204%200%200%200-2.2%202.2%204%204%200%200%200-3.12%200%204%204%200%200%200-2.2-2.2%204%204%200%200%200%200-3.1%204%204%200%200%200%202.2-2.2%22%2F%3E%3C%2Fsvg%3E";
15
+ // src/client/parts.tsx
16
+ function Logo({ size }) {
17
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
18
+ src: LOGO,
19
+ width: size,
20
+ height: size,
21
+ alt: "",
22
+ style: {
23
+ display: "block",
24
+ flexShrink: 0
25
+ }
26
+ });
27
+ }
28
+ const SHAPES = {
29
+ up: "M3.5 9.5 8 5l4.5 4.5",
30
+ down: "M3.5 6.5 8 11l4.5-4.5",
31
+ cross: "M4.5 4.5l7 7m0-7-7 7"
32
+ };
33
+ function Icon({ glyph }) {
34
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
35
+ viewBox: "0 0 16 16",
36
+ width: "14",
37
+ height: "14",
38
+ fill: "none",
39
+ stroke: "currentColor",
40
+ strokeWidth: "1.5",
41
+ strokeLinecap: "round",
42
+ strokeLinejoin: "round",
43
+ "aria-hidden": "true",
44
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: SHAPES[glyph] })
45
+ });
46
+ }
47
+ // src/client/remote.ts
48
+ async function request(path, body) {
49
+ const response = await fetch(new URL(`api/routerai/${path}`, document.baseURI), body === void 0 ? { headers: { accept: "application/json" } } : {
50
+ method: "POST",
51
+ headers: {
52
+ accept: "application/json",
53
+ "content-type": "application/json"
54
+ },
55
+ body: JSON.stringify(body)
56
+ });
57
+ const payload = await response.json().catch(() => null);
58
+ if (!response.ok || payload === null) throw new Error(payload?.error ?? `Сервер ответил ${response.status}`);
59
+ return payload;
60
+ }
61
+ const IDLE_REFRESH_MS = 45e3;
62
+ const WAITING_REFRESH_MS = 1200;
63
+ /** Состояние плагина в этой вкладке. Опрашивает сервер редко, а пока идёт вход — часто. */
64
+ var Remote = class {
65
+ view = {
66
+ overview: null,
67
+ failure: null
68
+ };
69
+ subscribers = /* @__PURE__ */ new Set();
70
+ timer;
71
+ catalogRequest;
72
+ stopped = false;
73
+ read = () => this.view;
74
+ subscribe = (subscriber) => {
75
+ this.subscribers.add(subscriber);
76
+ return () => {
77
+ this.subscribers.delete(subscriber);
78
+ };
79
+ };
80
+ /** Запустить опрос; возвращает остановку. */
81
+ run() {
82
+ this.pull();
83
+ return () => {
84
+ this.stopped = true;
85
+ clearTimeout(this.timer);
86
+ };
87
+ }
88
+ async pull(fresh = false) {
89
+ clearTimeout(this.timer);
90
+ try {
91
+ this.publish({
92
+ overview: await request(fresh ? "overview?fresh=1" : "overview"),
93
+ failure: null
94
+ });
95
+ } catch (error) {
96
+ this.publish({
97
+ ...this.view,
98
+ failure: error instanceof Error ? error.message : String(error)
99
+ });
100
+ }
101
+ if (this.stopped) return;
102
+ const waiting = this.view.overview?.signIn.stage === "waiting";
103
+ this.timer = setTimeout(() => {
104
+ this.pull(waiting);
105
+ }, waiting ? WAITING_REFRESH_MS : IDLE_REFRESH_MS);
106
+ }
107
+ publish(view) {
108
+ this.view = view;
109
+ for (const subscriber of this.subscribers) subscriber();
110
+ }
111
+ async act(path, body, fresh = true) {
112
+ await request(path, body);
113
+ await this.pull(fresh);
114
+ }
115
+ signIn = () => this.act("sign-in", {}, false);
116
+ cancelSignIn = () => this.act("sign-in/cancel", {}, false);
117
+ signOut = () => this.act("sign-out", {});
118
+ saveKey = (key) => this.act("key", { key });
119
+ savePreferences = (patch) => this.act("preferences", patch, false);
120
+ /** Каталог запрашивается один раз за жизнь вкладки. */
121
+ catalog() {
122
+ this.catalogRequest ??= request("catalog").then((reply) => reply.models);
123
+ this.catalogRequest.catch(() => {
124
+ this.catalogRequest = void 0;
125
+ });
126
+ return this.catalogRequest;
127
+ }
128
+ async sessionCost(sessionId) {
129
+ return (await request(`session-cost?id=${encodeURIComponent(sessionId)}`)).cost;
130
+ }
131
+ };
132
+ function useRemote(remote) {
133
+ return (0, react.useSyncExternalStore)(remote.subscribe, remote.read);
134
+ }
135
+ function openTab(url) {
136
+ window.open(url, "_blank", "noopener,noreferrer");
137
+ }
138
+ // src/client/MoneyNotice.tsx
139
+ const HEADLINES = {
140
+ "no-funds": "problem_no_funds",
141
+ "key-cap": "problem_key_cap",
142
+ "key-off": "problem_key_off"
143
+ };
144
+ /** `message` и `dismiss` передаёт harness: текст ошибки и закрытие уведомления. */
145
+ function MoneyNotice({ t, remote, message, dismiss }) {
146
+ const problem = remote.read().overview?.problem ?? null;
147
+ const funds = problem === null || problem.kind === "no-funds";
148
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
149
+ className: "rai-banner",
150
+ role: "alert",
151
+ "data-routerai": "money-notice",
152
+ children: [
153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 28 }),
154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t(HEADLINES[problem?.kind ?? "no-funds"] ?? "problem_no_funds") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
155
+ className: "rai-note",
156
+ children: problem?.text ?? message
157
+ })] }),
158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
159
+ variant: "primary",
160
+ size: "sm",
161
+ onClick: () => {
162
+ openTab(funds ? BILLING_PAGE : KEYS_PAGE);
163
+ },
164
+ children: funds ? t("addFunds") : t("openKeys")
165
+ }),
166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
167
+ variant: "ghost",
168
+ size: "sm",
169
+ onClick: () => {
170
+ remote.pull(true);
171
+ dismiss();
172
+ },
173
+ children: t("close")
174
+ })
175
+ ]
176
+ });
177
+ }
178
+ // src/money.ts
179
+ /** Сколько знаков после запятой показать, чтобы сумма не выглядела нулём и не шумела копейками. */
180
+ function fractionDigits(amount) {
181
+ const size = Math.abs(amount);
182
+ if (size === 0 || size >= 100) return 0;
183
+ if (size >= 1) return 2;
184
+ if (size >= .01) return 3;
185
+ return 4;
186
+ }
187
+ function rub(amount) {
188
+ if (amount === null || amount === void 0 || !Number.isFinite(amount)) return "—";
189
+ const digits = fractionDigits(amount);
190
+ return `${new Intl.NumberFormat("ru-RU", {
191
+ minimumFractionDigits: digits,
192
+ maximumFractionDigits: digits
193
+ }).format(amount)} ₽`;
194
+ }
195
+ /** «216 ₽ / 1 079 ₽» — вход и выход за миллион токенов. */
196
+ function pricePair(price) {
197
+ return price === void 0 ? "" : `${rub(price.input)} / ${rub(price.output)}`;
198
+ }
199
+ /** Размер контекстного окна: 1048576 → «1M», 200000 → «200K». */
200
+ function windowLabel(tokens) {
201
+ if (tokens === void 0) return "";
202
+ if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
203
+ return `${Math.round(tokens / 1e3)}K`;
204
+ }
205
+ // src/client/SessionCost.tsx
206
+ const REFRESH_MS = 4e3;
207
+ const count = (value) => value.toLocaleString("ru-RU");
208
+ function SessionCost({ t, remote, sessionId }) {
209
+ const [cost, setCost] = (0, react.useState)(null);
210
+ const [open, setOpen] = (0, react.useState)(false);
211
+ const anchor = (0, react.useRef)(null);
212
+ (0, react.useEffect)(() => {
213
+ setCost(null);
214
+ if (sessionId === void 0) return;
215
+ let active = true;
216
+ const refresh = () => {
217
+ if (document.hidden) return;
218
+ remote.sessionCost(sessionId).then((next) => {
219
+ if (active) setCost(next);
220
+ }, () => {});
221
+ };
222
+ refresh();
223
+ const timer = setInterval(refresh, REFRESH_MS);
224
+ return () => {
225
+ active = false;
226
+ clearInterval(timer);
227
+ };
228
+ }, [remote, sessionId]);
229
+ (0, react.useEffect)(() => {
230
+ if (!open) return;
231
+ const outside = (event) => {
232
+ if (!anchor.current?.contains(event.target)) setOpen(false);
233
+ };
234
+ const escape = (event) => {
235
+ if (event.key === "Escape") setOpen(false);
236
+ };
237
+ document.addEventListener("pointerdown", outside);
238
+ document.addEventListener("keydown", escape);
239
+ return () => {
240
+ document.removeEventListener("pointerdown", outside);
241
+ document.removeEventListener("keydown", escape);
242
+ };
243
+ }, [open]);
244
+ if (cost === null || cost.calls === 0) return null;
245
+ const models = Object.entries(cost.perModel).sort(([, a], [, b]) => b.rub - a.rub);
246
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
247
+ className: "rai-cost",
248
+ ref: anchor,
249
+ "data-routerai": "session-cost",
250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
251
+ type: "button",
252
+ "aria-expanded": open,
253
+ title: t("costTitle"),
254
+ onClick: () => {
255
+ setOpen((value) => !value);
256
+ },
257
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 13 }), rub(cost.total)]
258
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
259
+ className: "rai-sheet",
260
+ role: "dialog",
261
+ "aria-label": t("costTitle"),
262
+ children: [
263
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costTotal") }),
265
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: rub(cost.total) }),
266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costLast") }),
267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: rub(cost.last?.rub) }),
268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCalls") }),
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: cost.calls }),
270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCached") }),
271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: cost.cachedShare === null ? "—" : `${Math.round(cost.cachedShare * 100)}%` }),
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costInput") }),
273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.input + cost.tokens.cached) }),
274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costCacheWrites") }),
275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.cacheWrites) }),
276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("costOutput") }),
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(cost.tokens.output) })
278
+ ] }),
279
+ models.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: models.map(([slug, share]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
281
+ className: "rai-clip",
282
+ children: slug
283
+ }),
284
+ share.calls,
285
+ "× ",
286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: rub(share.rub) })
287
+ ] }, slug)) }) : null,
288
+ cost.unpriced > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
289
+ className: "rai-caution",
290
+ children: t("costUnpriced", { count: cost.unpriced })
291
+ }) : null,
292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
293
+ className: "rai-faint",
294
+ children: t("costNote")
295
+ })
296
+ ]
297
+ }) : null]
298
+ });
299
+ }
300
+ // src/starter.ts
301
+ const MODEL_SETS = [
302
+ {
303
+ key: "everyday",
304
+ models: [
305
+ "anthropic/claude-sonnet-5.5",
306
+ "deepseek/deepseek-v4.1-flash",
307
+ "openai/gpt-6.1-sol",
308
+ "google/gemini-4-argon",
309
+ "anthropic/claude-opus-5.5",
310
+ "moonshotai/kimi-k3.1-pro",
311
+ "z-ai/glm-5.3-flash"
312
+ ]
313
+ },
314
+ {
315
+ key: "coding",
316
+ models: [
317
+ "anthropic/claude-opus-5.5",
318
+ "google/gemini-4-argon",
319
+ "z-ai/glm-5.3-flash",
320
+ "openai/gpt-6.1-sol",
321
+ "moonshotai/kimi-k3.1-pro"
322
+ ]
323
+ },
324
+ {
325
+ key: "budget",
326
+ models: ["deepseek/deepseek-v4.1-flash", "z-ai/glm-5.3-flash"]
327
+ }
328
+ ];
329
+ MODEL_SETS[0].models;
330
+ // src/client/SettingsPage.tsx
331
+ /** Выполнить действие и показать его ошибку рядом с кнопкой, а не потерять в консоли. */
332
+ function useAction() {
333
+ const [busy, setBusy] = (0, react.useState)(false);
334
+ const [error, setError] = (0, react.useState)(null);
335
+ const run = (action) => {
336
+ setBusy(true);
337
+ setError(null);
338
+ action().catch((problem) => {
339
+ setError(problem instanceof Error ? problem.message : String(problem));
340
+ }).finally(() => {
341
+ setBusy(false);
342
+ });
343
+ };
344
+ return {
345
+ busy,
346
+ error,
347
+ run
348
+ };
349
+ }
350
+ function Panel({ title, hint, children }) {
351
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
352
+ className: "rai-panel",
353
+ children: [
354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: title }),
355
+ hint === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
356
+ className: "rai-note",
357
+ children: hint
358
+ }),
359
+ children
360
+ ]
361
+ });
362
+ }
363
+ function Connect({ t, remote, overview }) {
364
+ const [draft, setDraft] = (0, react.useState)("");
365
+ const action = useAction();
366
+ const stage = overview.signIn;
367
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
368
+ title: t("connectTitle"),
369
+ hint: stage.stage === "waiting" ? t("connectWaiting") : t("connectHint"),
370
+ children: [
371
+ stage.stage === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
372
+ className: "rai-line",
373
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
374
+ variant: "outline",
375
+ onClick: () => {
376
+ openTab(stage.url);
377
+ },
378
+ children: t("connectReopen")
379
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
380
+ variant: "ghost",
381
+ onClick: () => {
382
+ action.run(remote.cancelSignIn);
383
+ },
384
+ children: t("connectAbort")
385
+ })]
386
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
387
+ className: "rai-line",
388
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
389
+ variant: "primary",
390
+ disabled: action.busy,
391
+ onClick: () => {
392
+ action.run(remote.signIn);
393
+ },
394
+ children: t("connectButton")
395
+ })
396
+ }),
397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
+ className: "rai-line",
399
+ children: [
400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
401
+ className: "rai-note",
402
+ children: t("pasteLabel")
403
+ }),
404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
405
+ className: "rai-fill",
406
+ type: "password",
407
+ autoComplete: "off",
408
+ placeholder: t("pasteHolder"),
409
+ value: draft,
410
+ onChange: (event) => {
411
+ setDraft(event.target.value);
412
+ }
413
+ }),
414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
415
+ variant: "outline",
416
+ disabled: action.busy || draft.trim() === "",
417
+ onClick: () => {
418
+ action.run(() => remote.saveKey(draft.trim()).then(() => {
419
+ setDraft("");
420
+ }));
421
+ },
422
+ children: t("pasteButton")
423
+ })
424
+ ]
425
+ }),
426
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
427
+ className: "rai-alert",
428
+ children: action.error
429
+ }) : stage.stage === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
430
+ className: "rai-alert",
431
+ children: stage.reason
432
+ }) : null
433
+ ]
434
+ });
435
+ }
436
+ function Account({ t, remote, overview }) {
437
+ const action = useAction();
438
+ const account = overview.account;
439
+ const key = account?.key;
440
+ const cap = key?.cap;
441
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
442
+ className: "rai-panel",
443
+ "data-routerai": "account",
444
+ children: [
445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
446
+ className: "rai-line",
447
+ children: [
448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
+ className: "rai-fill",
450
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
451
+ className: "rai-clip",
452
+ style: {
453
+ margin: 0,
454
+ font: "var(--dsw-font-s-strong-14)"
455
+ },
456
+ children: key?.label || t("accountNoLabel")
457
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
458
+ className: "rai-faint",
459
+ children: overview.keyMask
460
+ })]
461
+ }),
462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
463
+ variant: "outline",
464
+ onClick: () => {
465
+ openTab(BILLING_PAGE);
466
+ },
467
+ children: t("addFunds")
468
+ }),
469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
470
+ variant: "ghost",
471
+ disabled: action.busy,
472
+ onClick: () => {
473
+ action.run(() => remote.pull(true));
474
+ },
475
+ children: t("reload")
476
+ }),
477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
478
+ variant: "ghost",
479
+ disabled: action.busy,
480
+ onClick: () => {
481
+ action.run(remote.signOut);
482
+ },
483
+ children: t("disconnect")
484
+ })
485
+ ]
486
+ }),
487
+ account?.state === "rejected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
488
+ className: "rai-alert",
489
+ children: t("keyRejected")
490
+ }) : null,
491
+ account?.state === "offline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
492
+ className: "rai-caution",
493
+ children: t("offline")
494
+ }) : null,
495
+ key?.disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
496
+ className: "rai-alert",
497
+ children: t("keyDisabled")
498
+ }) : null,
499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
500
+ className: "rai-figures",
501
+ children: [
502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
503
+ className: "rai-figure rai-main",
504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
505
+ className: "rai-note",
506
+ children: t("balance")
507
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(account?.balance) })]
508
+ }),
509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
510
+ className: "rai-figure",
511
+ children: [
512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
513
+ className: "rai-note",
514
+ children: t("keyCap")
515
+ }),
516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: cap === void 0 ? t("keyCapNone") : rub(cap.left) }),
517
+ cap === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
518
+ className: "rai-faint",
519
+ children: t("keyCapLeft", {
520
+ amount: rub(cap.amount),
521
+ period: t(`period_${cap.period}`)
522
+ })
523
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
524
+ className: "rai-gauge",
525
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(100 * Math.min(1, Math.max(0, 1 - cap.left / cap.amount)))}%` } })
526
+ })] })
527
+ ]
528
+ }),
529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
530
+ className: "rai-figure",
531
+ children: [
532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
533
+ className: "rai-note",
534
+ children: t("keySpent")
535
+ }),
536
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(key?.spent ?? 0) }),
537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
538
+ className: "rai-faint",
539
+ children: t("keySpentMonth", { amount: rub(key?.spentThisMonth ?? 0) })
540
+ })
541
+ ]
542
+ })
543
+ ]
544
+ }),
545
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
546
+ className: "rai-alert",
547
+ children: action.error
548
+ }) : null
549
+ ]
550
+ });
551
+ }
552
+ function modelFacts(entry) {
553
+ return [
554
+ entry.slug,
555
+ pricePair(entry.price),
556
+ windowLabel(entry.window)
557
+ ].filter((part) => part !== "").join(" · ");
558
+ }
559
+ function Picker({ t, remote, overview, catalog }) {
560
+ const saved = overview.picker.models;
561
+ const savedKey = saved.join("|");
562
+ const [draft, setDraft] = (0, react.useState)(saved);
563
+ const [search, setSearch] = (0, react.useState)("");
564
+ const [maker, setMaker] = (0, react.useState)("");
565
+ const [toolsOnly, setToolsOnly] = (0, react.useState)(true);
566
+ const [justApplied, setJustApplied] = (0, react.useState)(false);
567
+ const action = useAction();
568
+ (0, react.useEffect)(() => {
569
+ setDraft(saved);
570
+ }, [savedKey]);
571
+ const bySlug = (0, react.useMemo)(() => new Map((catalog ?? []).map((entry) => [entry.slug, entry])), [catalog]);
572
+ const makers = (0, react.useMemo)(() => [...new Set((catalog ?? []).map((entry) => entry.maker))].sort((a, b) => a.localeCompare(b)), [catalog]);
573
+ const matches = (0, react.useMemo)(() => {
574
+ const needle = search.trim().toLowerCase();
575
+ return (catalog ?? []).filter((entry) => (maker === "" || entry.maker === maker) && (!toolsOnly || entry.tools) && (needle === "" || `${entry.slug} ${entry.maker} ${entry.title}`.toLowerCase().includes(needle)));
576
+ }, [
577
+ catalog,
578
+ search,
579
+ maker,
580
+ toolsOnly
581
+ ]);
582
+ const changed = draft.join("|") !== savedKey;
583
+ const edit = (next) => {
584
+ setJustApplied(false);
585
+ setDraft(next);
586
+ };
587
+ const shift = (from, to) => {
588
+ const next = [...draft];
589
+ next.splice(to, 0, ...next.splice(from, 1));
590
+ edit(next);
591
+ };
592
+ const toggle = (slug) => {
593
+ edit(draft.includes(slug) ? draft.filter((item) => item !== slug) : [...draft, slug]);
594
+ };
595
+ const visible = matches.slice(0, 250);
596
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
597
+ title: t("pickerTitle"),
598
+ hint: t("pickerHint"),
599
+ children: [
600
+ draft.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
601
+ className: "rai-note",
602
+ children: t("pickerEmpty")
603
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
604
+ className: "rai-rows",
605
+ "data-routerai": "picked",
606
+ children: draft.map((slug, position) => {
607
+ const entry = bySlug.get(slug);
608
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
609
+ className: "rai-row",
610
+ children: [
611
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
612
+ className: "rai-order",
613
+ children: position + 1
614
+ }),
615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
616
+ style: { minWidth: 0 },
617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
618
+ className: "rai-clip",
619
+ children: entry === void 0 ? slug : `${entry.maker} ${entry.title}`
620
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
621
+ className: "rai-faint rai-clip",
622
+ children: entry === void 0 ? catalog === null ? slug : `${slug} · ${t("pickerUnknown")}` : modelFacts(entry)
623
+ })]
624
+ }),
625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
626
+ className: "rai-tools",
627
+ children: [
628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
629
+ type: "button",
630
+ title: t("moveUp"),
631
+ "aria-label": t("moveUp"),
632
+ disabled: position === 0,
633
+ onClick: () => {
634
+ shift(position, position - 1);
635
+ },
636
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "up" })
637
+ }),
638
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
639
+ type: "button",
640
+ title: t("moveDown"),
641
+ "aria-label": t("moveDown"),
642
+ disabled: position === draft.length - 1,
643
+ onClick: () => {
644
+ shift(position, position + 1);
645
+ },
646
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "down" })
647
+ }),
648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
649
+ type: "button",
650
+ title: t("drop"),
651
+ "aria-label": t("drop"),
652
+ onClick: () => {
653
+ toggle(slug);
654
+ },
655
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { glyph: "cross" })
656
+ })
657
+ ]
658
+ })
659
+ ]
660
+ }, slug);
661
+ })
662
+ }),
663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
664
+ className: "rai-line",
665
+ children: [
666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
667
+ variant: "primary",
668
+ disabled: !changed || action.busy,
669
+ onClick: () => {
670
+ action.run(() => remote.savePreferences({ models: draft }).then(() => {
671
+ setJustApplied(true);
672
+ }));
673
+ },
674
+ children: t("apply")
675
+ }),
676
+ changed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
677
+ variant: "ghost",
678
+ onClick: () => {
679
+ edit(saved);
680
+ },
681
+ children: t("revert")
682
+ }) : null,
683
+ justApplied && !changed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
684
+ className: "rai-note",
685
+ role: "status",
686
+ children: t("applied")
687
+ }) : null,
688
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
689
+ className: "rai-alert",
690
+ children: action.error
691
+ }) : null
692
+ ]
693
+ }),
694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
695
+ className: "rai-line",
696
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
697
+ className: "rai-note",
698
+ children: [t("sets"), ":"]
699
+ }), MODEL_SETS.map((set) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
700
+ variant: "outline",
701
+ size: "sm",
702
+ disabled: catalog === null,
703
+ onClick: () => {
704
+ edit(set.models.filter((slug) => bySlug.has(slug)));
705
+ },
706
+ children: t(`set_${set.key}`)
707
+ }, set.key))]
708
+ }),
709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
710
+ style: {
711
+ margin: "6px 0 0",
712
+ font: "var(--dsw-font-s-strong-14)"
713
+ },
714
+ children: t("catalogTitle")
715
+ }),
716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
717
+ className: "rai-line",
718
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
719
+ className: "rai-fill",
720
+ placeholder: t("catalogSearch"),
721
+ value: search,
722
+ onChange: (event) => {
723
+ setSearch(event.target.value);
724
+ }
725
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
726
+ className: "rai-choice",
727
+ value: maker,
728
+ "aria-label": t("catalogAnyMaker"),
729
+ onChange: (event) => {
730
+ setMaker(event.target.value);
731
+ },
732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
733
+ value: "",
734
+ children: t("catalogAnyMaker")
735
+ }), makers.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
736
+ value: item,
737
+ children: item
738
+ }, item))]
739
+ })]
740
+ }),
741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
742
+ className: "rai-line",
743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
744
+ className: "rai-line rai-note",
745
+ style: { cursor: "pointer" },
746
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
747
+ type: "checkbox",
748
+ checked: toolsOnly,
749
+ onChange: (event) => {
750
+ setToolsOnly(event.target.checked);
751
+ }
752
+ }), t("catalogToolsOnly")]
753
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
754
+ className: "rai-faint",
755
+ style: { marginLeft: "auto" },
756
+ children: catalog === null ? t("catalogLoading") : t("catalogShown", {
757
+ shown: visible.length,
758
+ total: matches.length
759
+ })
760
+ })]
761
+ }),
762
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
763
+ className: "rai-rows rai-tall",
764
+ "data-routerai": "catalog",
765
+ children: visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
766
+ className: "rai-row",
767
+ children: [
768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
769
+ type: "checkbox",
770
+ checked: draft.includes(entry.slug),
771
+ onChange: () => {
772
+ toggle(entry.slug);
773
+ }
774
+ }),
775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
776
+ style: { minWidth: 0 },
777
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
778
+ className: "rai-clip",
779
+ children: [
780
+ entry.maker,
781
+ " ",
782
+ entry.title
783
+ ]
784
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
785
+ className: "rai-faint rai-clip",
786
+ children: modelFacts(entry)
787
+ })]
788
+ }),
789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
790
+ className: "rai-line",
791
+ children: [
792
+ entry.alias ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
793
+ tone: "quiet",
794
+ children: t("tagAlias")
795
+ }) : null,
796
+ entry.reasoning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
797
+ tone: "quiet",
798
+ children: t("tagReasoning")
799
+ }) : null,
800
+ entry.vision ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
801
+ tone: "quiet",
802
+ children: t("tagVision")
803
+ }) : null,
804
+ entry.tools ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
805
+ tone: "warning",
806
+ children: t("tagNoTools")
807
+ })
808
+ ]
809
+ })
810
+ ]
811
+ }, entry.slug))
812
+ })
813
+ ]
814
+ });
815
+ }
816
+ function Extras({ t, remote, overview, catalog }) {
817
+ const [threshold, setThreshold] = (0, react.useState)(String(overview.lowBalance));
818
+ const action = useAction();
819
+ const cheapestFirst = (0, react.useMemo)(() => (catalog ?? []).filter((entry) => entry.tools && entry.price !== void 0 && !entry.alias).sort((a, b) => (a.price?.input ?? 0) - (b.price?.input ?? 0)), [catalog]);
820
+ const current = overview.utilityModel;
821
+ const commitThreshold = () => {
822
+ const value = Number(threshold.replace(",", "."));
823
+ if (Number.isFinite(value) && value >= 0) action.run(() => remote.savePreferences({ lowBalance: value }));
824
+ else setThreshold(String(overview.lowBalance));
825
+ };
826
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Panel, {
827
+ title: t("extrasTitle"),
828
+ children: [
829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
830
+ style: {
831
+ display: "grid",
832
+ gap: 6
833
+ },
834
+ children: [
835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("utilityLabel") }),
836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
837
+ className: "rai-choice",
838
+ value: current ?? "",
839
+ onChange: (event) => {
840
+ action.run(() => remote.savePreferences({ utilityModel: event.target.value === "" ? null : event.target.value }));
841
+ },
842
+ children: [
843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
844
+ value: "",
845
+ children: t("utilityOff")
846
+ }),
847
+ current !== null && !cheapestFirst.some((entry) => entry.slug === current) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
848
+ value: current,
849
+ children: current
850
+ }) : null,
851
+ cheapestFirst.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
852
+ value: entry.slug,
853
+ children: [
854
+ entry.maker,
855
+ " ",
856
+ entry.title,
857
+ " · ",
858
+ pricePair(entry.price)
859
+ ]
860
+ }, entry.slug))
861
+ ]
862
+ }),
863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
864
+ className: "rai-note",
865
+ children: t("utilityHint")
866
+ })
867
+ ]
868
+ }),
869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
870
+ className: "rai-line",
871
+ children: [
872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
873
+ className: "rai-fill",
874
+ children: t("lowBalanceLabel")
875
+ }),
876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
877
+ style: { width: 96 },
878
+ inputMode: "decimal",
879
+ value: threshold,
880
+ onChange: (event) => {
881
+ setThreshold(event.target.value);
882
+ },
883
+ onBlur: commitThreshold
884
+ }),
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "₽" })
886
+ ]
887
+ }),
888
+ action.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
889
+ className: "rai-alert",
890
+ children: action.error
891
+ }) : null
892
+ ]
893
+ });
894
+ }
895
+ function SettingsPage({ t, remote }) {
896
+ const { overview, failure } = useRemote(remote);
897
+ const [catalog, setCatalog] = (0, react.useState)(null);
898
+ const [catalogFailure, setCatalogFailure] = (0, react.useState)(null);
899
+ (0, react.useEffect)(() => {
900
+ let current = true;
901
+ remote.catalog().then((entries) => {
902
+ if (current) setCatalog(entries);
903
+ }, (problem) => {
904
+ if (current) setCatalogFailure(problem instanceof Error ? problem.message : String(problem));
905
+ });
906
+ return () => {
907
+ current = false;
908
+ };
909
+ }, [remote]);
910
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
911
+ className: "rai-page",
912
+ children: [
913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
914
+ className: "rai-head",
915
+ children: [
916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 40 }),
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("heading") }),
918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("lead") })
919
+ ]
920
+ }),
921
+ failure !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
922
+ className: "rai-alert",
923
+ children: failure
924
+ }) : null,
925
+ overview === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
926
+ overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
927
+ t,
928
+ remote,
929
+ overview
930
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Connect, {
931
+ t,
932
+ remote,
933
+ overview
934
+ }),
935
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
936
+ t,
937
+ remote,
938
+ overview,
939
+ catalog
940
+ }),
941
+ catalogFailure !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
942
+ className: "rai-alert",
943
+ children: catalogFailure
944
+ }) : null,
945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Extras, {
946
+ t,
947
+ remote,
948
+ overview,
949
+ catalog
950
+ })
951
+ ] })
952
+ ]
953
+ });
954
+ }
955
+ // src/client/SidebarBalance.tsx
956
+ /** `wide` сообщает harness: развёрнута ли боковая панель. */
957
+ function SidebarBalance({ t, remote, wide }) {
958
+ const { overview } = useRemote(remote);
959
+ const [shown, setShown] = (0, react.useState)(false);
960
+ if (overview === null) return null;
961
+ const balance = overview.account?.balance;
962
+ const running = overview.problem !== null || balance !== void 0 && balance < overview.lowBalance;
963
+ const cap = overview.account?.key?.cap;
964
+ const classes = [
965
+ "rai-side",
966
+ wide ? "" : "rai-compact",
967
+ running ? "rai-warn" : ""
968
+ ].join(" ").trim();
969
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
970
+ type: "button",
971
+ className: classes,
972
+ "data-routerai": "sidebar-balance",
973
+ title: t("sidebarTitle"),
974
+ onClick: () => {
975
+ setShown(true);
976
+ remote.pull(true);
977
+ },
978
+ children: [
979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size: 16 }),
980
+ wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sidebarTitle") }) : null,
981
+ wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: overview.connected ? rub(balance) : t("sidebarConnect") }) : null
982
+ ]
983
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
984
+ open: shown,
985
+ onClose: () => {
986
+ setShown(false);
987
+ },
988
+ title: t("sidebarTitle"),
989
+ closeLabel: t("close"),
990
+ footer: overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
991
+ variant: "primary",
992
+ onClick: () => {
993
+ openTab(BILLING_PAGE);
994
+ },
995
+ children: t("addFunds")
996
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
997
+ variant: "primary",
998
+ onClick: () => {
999
+ remote.signIn();
1000
+ },
1001
+ children: t("connectButton")
1002
+ }),
1003
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1004
+ style: {
1005
+ display: "grid",
1006
+ gap: 12
1007
+ },
1008
+ children: [
1009
+ overview.problem !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1010
+ className: "rai-alert",
1011
+ children: overview.problem.text
1012
+ }) : running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1013
+ className: "rai-caution",
1014
+ children: t("sidebarLow")
1015
+ }) : null,
1016
+ overview.connected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1017
+ className: "rai-figures",
1018
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1019
+ className: "rai-figure rai-main",
1020
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1021
+ className: "rai-note",
1022
+ children: t("balance")
1023
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: rub(balance) })]
1024
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1025
+ className: "rai-figure",
1026
+ children: [
1027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1028
+ className: "rai-note",
1029
+ children: t("keyCap")
1030
+ }),
1031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: cap === void 0 ? t("keyCapNone") : rub(cap.left) }),
1032
+ cap === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1033
+ className: "rai-faint",
1034
+ children: t("keyCapLeft", {
1035
+ amount: rub(cap.amount),
1036
+ period: t(`period_${cap.period}`)
1037
+ })
1038
+ })
1039
+ ]
1040
+ })]
1041
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1042
+ className: "rai-note",
1043
+ children: t("connectHint")
1044
+ }),
1045
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1046
+ className: "rai-faint",
1047
+ children: t("sidebarHint")
1048
+ })
1049
+ ]
1050
+ })
1051
+ })] });
1052
+ }
1053
+ // src/client/styles.ts
1054
+ const ACCENT = "#2d67f6";
1055
+ const SHEET = `
1056
+ .rai-page{display:grid;gap:18px;padding-bottom:28px;font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary)}
1057
+ .rai-head{display:grid;grid-template-columns:auto 1fr;column-gap:12px;align-items:center}
1058
+ .rai-head h2{margin:0;font:var(--dsw-font-l-20)}
1059
+ .rai-head p{margin:0;grid-column:2;color:var(--dsw-alias-label-secondary)}
1060
+ .rai-panel{display:grid;gap:12px;padding:16px;border-radius:var(--dsw-radius-md);border:1px solid var(--dsw-alias-settings-card-stroke);background:var(--dsw-alias-settings-card-fill)}
1061
+ .rai-panel>h3{margin:0;font:var(--dsw-font-s-strong-14)}
1062
+ .rai-note{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-secondary)}
1063
+ .rai-faint{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-tertiary)}
1064
+ .rai-alert{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-state-error-primary)}
1065
+ .rai-caution{margin:0;font:var(--dsw-font-xs-13);color:var(--dsw-alias-state-warn-label)}
1066
+ .rai-line{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1067
+ .rai-line>.rai-fill{flex:1 1 160px;min-width:0}
1068
+ .rai-clip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1069
+ .rai-figures{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}
1070
+ .rai-figure{display:grid;gap:2px;padding:10px 12px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3)}
1071
+ .rai-figure>strong{font:var(--dsw-font-l-20)}
1072
+ .rai-figure.rai-main{border-color:${ACCENT}}
1073
+ .rai-gauge{height:3px;border-radius:2px;background:var(--dsw-alias-border-l2)}
1074
+ .rai-gauge>span{display:block;height:100%;border-radius:2px;background:${ACCENT}}
1075
+ .rai-rows{display:grid;border-radius:var(--dsw-radius-sm);border:1px solid var(--dsw-alias-border-l3);overflow:hidden}
1076
+ .rai-rows.rai-tall{max-height:380px;overflow-y:auto}
1077
+ .rai-row{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;padding:8px 10px}
1078
+ .rai-row+.rai-row{border-top:1px solid var(--dsw-alias-border-l2)}
1079
+ .rai-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
1080
+ label.rai-row{cursor:pointer}
1081
+ .rai-row input[type=checkbox]{width:16px;height:16px;margin:0;accent-color:${ACCENT}}
1082
+ .rai-order{width:1.5em;text-align:right;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xs-13)}
1083
+ .rai-tools{display:flex;gap:2px}
1084
+ .rai-tools button{all:unset;display:grid;place-items:center;width:26px;height:26px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-secondary)}
1085
+ .rai-tools button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1086
+ .rai-tools button:disabled{opacity:.3;cursor:default}
1087
+ .rai-choice{height:36px;padding:0 8px;font:inherit;color:inherit;border-radius:var(--dsw-radius-sm);border:1px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-1)}
1088
+ .rai-side{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:8px;width:100%;padding:6px 8px;border-radius:var(--dsw-radius-sm);cursor:pointer;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-secondary)}
1089
+ .rai-side:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1090
+ .rai-side>span{flex:1}
1091
+ .rai-side>b{font-weight:500;color:var(--dsw-alias-label-primary)}
1092
+ .rai-side.rai-compact{width:auto;justify-content:center}
1093
+ .rai-side.rai-warn,.rai-side.rai-warn>b{color:var(--dsw-alias-state-warn-label)}
1094
+ .rai-cost{position:relative;display:inline-flex}
1095
+ .rai-cost>button{all:unset;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:12px;cursor:pointer;white-space:nowrap;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
1096
+ .rai-cost>button:hover{color:var(--dsw-alias-label-primary)}
1097
+ .rai-sheet{position:absolute;z-index:60;right:0;bottom:calc(100% + 8px);width:300px;max-width:calc(100vw - 24px);box-sizing:border-box;display:grid;gap:8px;padding:12px 14px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgba(0,0,0,.18)}
1098
+ .rai-sheet dl{display:grid;grid-template-columns:1fr auto;gap:4px 14px;margin:0}
1099
+ .rai-sheet dt{color:var(--dsw-alias-label-secondary)}
1100
+ .rai-sheet dd{margin:0;text-align:right;font-weight:500}
1101
+ .rai-sheet ul{display:grid;gap:2px;margin:0;padding:6px 0 0;list-style:none;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}
1102
+ .rai-sheet li{display:flex;gap:8px}
1103
+ .rai-sheet li>span{flex:1;min-width:0}
1104
+ .rai-banner{display:flex;align-items:center;gap:12px;max-width:540px;padding:12px 14px;border-radius:var(--dsw-radius-md);font:var(--dsw-font-s-14);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 8px 24px rgba(0,0,0,.14)}
1105
+ .rai-banner>div{flex:1;min-width:0;display:grid;gap:2px}
1106
+ `;
1107
+ /** Подключить стили. Атрибуты data-plugin* — соглашение harness: по ним он снимает стили выгруженного плагина. */
1108
+ function mountStyles(owner) {
1109
+ const element = document.createElement("style");
1110
+ element.dataset["plugin"] = owner;
1111
+ element.dataset["pluginCss"] = `${owner}/styles`;
1112
+ element.textContent = SHEET;
1113
+ document.head.append(element);
1114
+ return () => {
1115
+ element.remove();
1116
+ };
1117
+ }
1118
+ // src/client/texts.ts
1119
+ const NAMESPACE = "routerai";
1120
+ const russian = {
1121
+ section: "RouterAI",
1122
+ heading: "RouterAI",
1123
+ lead: "Модели разных производителей по одному ключу, оплата в рублях.",
1124
+ connectTitle: "Подключение",
1125
+ connectHint: "Нажмите «Войти» — откроется routerai.ru, где вы подтвердите доступ и зададите лимит расходов. Ключ сохранится сам.",
1126
+ connectButton: "Войти через RouterAI",
1127
+ connectWaiting: "Подтвердите доступ во вкладке браузера, которая только что открылась.",
1128
+ connectReopen: "Открыть вкладку снова",
1129
+ connectAbort: "Прервать",
1130
+ pasteLabel: "Уже есть ключ?",
1131
+ pasteHolder: "sk-…",
1132
+ pasteButton: "Сохранить",
1133
+ accountNoLabel: "Ключ RouterAI",
1134
+ balance: "На счёте",
1135
+ keyCap: "Остаток лимита ключа",
1136
+ keyCapLeft: "из {amount} {period}",
1137
+ keyCapNone: "не задан",
1138
+ keySpent: "Потрачено по ключу",
1139
+ keySpentMonth: "{amount} за месяц",
1140
+ addFunds: "Пополнить",
1141
+ reload: "Обновить",
1142
+ disconnect: "Выйти",
1143
+ keyRejected: "RouterAI больше не принимает этот ключ. Войдите заново.",
1144
+ keyDisabled: "Ключ отключён в кабинете RouterAI.",
1145
+ offline: "Нет связи с RouterAI — данные аккаунта недоступны.",
1146
+ pickerTitle: "Модели в пикере",
1147
+ pickerHint: "Порядок здесь — порядок в пикере. Цены указаны в рублях за миллион токенов: вход / выход.",
1148
+ pickerEmpty: "Ничего не выбрано. Отметьте модели в каталоге ниже.",
1149
+ pickerUnknown: "в каталоге не найдена",
1150
+ moveUp: "Поднять",
1151
+ moveDown: "Опустить",
1152
+ drop: "Убрать из пикера",
1153
+ apply: "Применить",
1154
+ applied: "Изменения сохранены",
1155
+ revert: "Отменить правки",
1156
+ sets: "Готовые наборы",
1157
+ set_everyday: "На каждый день",
1158
+ set_coding: "Для кода",
1159
+ set_budget: "Экономный",
1160
+ catalogTitle: "Каталог RouterAI",
1161
+ catalogSearch: "Название, производитель или идентификатор",
1162
+ catalogAnyMaker: "Любой производитель",
1163
+ catalogToolsOnly: "Скрыть модели без инструментов",
1164
+ catalogShown: "Показано {shown} из {total}",
1165
+ catalogLoading: "Загружаю каталог…",
1166
+ tagReasoning: "думает",
1167
+ tagVision: "видит картинки",
1168
+ tagNoTools: "без инструментов",
1169
+ tagAlias: "всегда последняя версия",
1170
+ extrasTitle: "Дополнительно",
1171
+ utilityLabel: "Служебная модель",
1172
+ utilityHint: "На ней придумываются названия сессий и сжимается длинный контекст. Дешёвая модель экономит деньги и не влияет на ответы.",
1173
+ utilityOff: "Не использовать — всё делает модель разговора",
1174
+ lowBalanceLabel: "Предупреждать о балансе ниже",
1175
+ sidebarConnect: "Войти",
1176
+ sidebarTitle: "RouterAI",
1177
+ sidebarLow: "Баланс подходит к концу.",
1178
+ sidebarHint: "Модели и ключ настраиваются в Настройках → RouterAI.",
1179
+ close: "Закрыть",
1180
+ costTitle: "Стоимость сессии",
1181
+ costTotal: "Всего",
1182
+ costLast: "Последний запрос",
1183
+ costCalls: "Запросов к модели",
1184
+ costCached: "Вход из кэша",
1185
+ costInput: "Вход, токены",
1186
+ costCacheWrites: "Записано в кэш, токены",
1187
+ costOutput: "Выход, токены",
1188
+ costUnpriced: "Запросов без цены в каталоге: {count}",
1189
+ costNote: "Расчёт по числу токенов и ценам каталога. Точные списания — в кабинете RouterAI.",
1190
+ problem_no_funds: "На балансе RouterAI закончились деньги",
1191
+ problem_key_cap: "Ключ RouterAI исчерпал лимит расходов",
1192
+ problem_key_off: "Ключ RouterAI отключён",
1193
+ openKeys: "К ключам",
1194
+ period_daily: "в сутки",
1195
+ period_weekly: "в неделю",
1196
+ period_monthly: "в месяц",
1197
+ period_other: ""
1198
+ };
1199
+ const english = {
1200
+ section: "RouterAI",
1201
+ heading: "RouterAI",
1202
+ lead: "Models from many vendors behind one key, billed in rubles.",
1203
+ connectTitle: "Connection",
1204
+ connectHint: "Press “Sign in”: routerai.ru opens, you approve access and set a spending limit. The key is saved for you.",
1205
+ connectButton: "Sign in with RouterAI",
1206
+ connectWaiting: "Approve access in the browser tab that has just opened.",
1207
+ connectReopen: "Open the tab again",
1208
+ connectAbort: "Stop",
1209
+ pasteLabel: "Already have a key?",
1210
+ pasteHolder: "sk-…",
1211
+ pasteButton: "Save",
1212
+ accountNoLabel: "RouterAI key",
1213
+ balance: "Balance",
1214
+ keyCap: "Key limit left",
1215
+ keyCapLeft: "of {amount} {period}",
1216
+ keyCapNone: "not set",
1217
+ keySpent: "Spent with this key",
1218
+ keySpentMonth: "{amount} this month",
1219
+ addFunds: "Add funds",
1220
+ reload: "Refresh",
1221
+ disconnect: "Sign out",
1222
+ keyRejected: "RouterAI no longer accepts this key. Sign in again.",
1223
+ keyDisabled: "The key is disabled in the RouterAI dashboard.",
1224
+ offline: "RouterAI is unreachable — account data is unavailable.",
1225
+ pickerTitle: "Models in the picker",
1226
+ pickerHint: "The order here is the order in the picker. Prices are rubles per million tokens: input / output.",
1227
+ pickerEmpty: "Nothing selected. Tick models in the catalog below.",
1228
+ pickerUnknown: "not found in the catalog",
1229
+ moveUp: "Move up",
1230
+ moveDown: "Move down",
1231
+ drop: "Remove from the picker",
1232
+ apply: "Apply",
1233
+ applied: "Changes saved",
1234
+ revert: "Discard edits",
1235
+ sets: "Ready-made sets",
1236
+ set_everyday: "Everyday",
1237
+ set_coding: "Coding",
1238
+ set_budget: "Budget",
1239
+ catalogTitle: "RouterAI catalog",
1240
+ catalogSearch: "Name, vendor or identifier",
1241
+ catalogAnyMaker: "Any vendor",
1242
+ catalogToolsOnly: "Hide models without tool calling",
1243
+ catalogShown: "Showing {shown} of {total}",
1244
+ catalogLoading: "Loading the catalog…",
1245
+ tagReasoning: "reasoning",
1246
+ tagVision: "reads images",
1247
+ tagNoTools: "no tools",
1248
+ tagAlias: "always the latest version",
1249
+ extrasTitle: "More",
1250
+ utilityLabel: "Utility model",
1251
+ utilityHint: "It names sessions and compacts long context. A cheap model saves money and does not affect answers.",
1252
+ utilityOff: "Off — the conversation model does everything",
1253
+ lowBalanceLabel: "Warn when the balance is below",
1254
+ sidebarConnect: "Sign in",
1255
+ sidebarTitle: "RouterAI",
1256
+ sidebarLow: "The balance is running out.",
1257
+ sidebarHint: "Models and the key are configured in Settings → RouterAI.",
1258
+ close: "Close",
1259
+ costTitle: "Session cost",
1260
+ costTotal: "Total",
1261
+ costLast: "Last request",
1262
+ costCalls: "Model requests",
1263
+ costCached: "Input served from cache",
1264
+ costInput: "Input tokens",
1265
+ costCacheWrites: "Tokens written to cache",
1266
+ costOutput: "Output tokens",
1267
+ costUnpriced: "Requests with no catalog price: {count}",
1268
+ costNote: "Calculated from token counts and catalog prices. Exact charges are in the RouterAI dashboard.",
1269
+ problem_no_funds: "The RouterAI balance has run out",
1270
+ problem_key_cap: "The RouterAI key has reached its spending limit",
1271
+ problem_key_off: "The RouterAI key is disabled",
1272
+ openKeys: "Open keys",
1273
+ period_daily: "per day",
1274
+ period_weekly: "per week",
1275
+ period_monthly: "per month",
1276
+ period_other: ""
1277
+ };
1278
+ const DICTIONARIES = {
1279
+ ru: russian,
1280
+ en: english,
1281
+ zh: english
1282
+ };
1283
+ // src/client/main.tsx
1284
+ const name = "routerai-client";
1285
+ const inject = ["slots", "locale"];
1286
+ const PACKAGE = "@router-ai/dsh-plugin-routerai";
1287
+ function apply(ctx) {
1288
+ const slots = ctx.slots;
1289
+ const locale = ctx.locale;
1290
+ ctx.effect(() => locale.register(NAMESPACE, DICTIONARIES), "routerai: texts");
1291
+ ctx.effect(() => mountStyles(PACKAGE), "routerai: styles");
1292
+ const t = locale.bind(NAMESPACE);
1293
+ const remote = new Remote();
1294
+ ctx.effect(() => remote.run(), "routerai: remote state");
1295
+ const shared = () => ({ remote });
1296
+ const placements = [
1297
+ {
1298
+ slot: "settings.section",
1299
+ options: {
1300
+ id: "routerai",
1301
+ order: 5,
1302
+ label: () => t("section"),
1303
+ inject: shared
1304
+ },
1305
+ component: SettingsPage
1306
+ },
1307
+ {
1308
+ slot: "settings.section.icon",
1309
+ options: { id: "routerai" },
1310
+ component: ({ size }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Logo, { size })
1311
+ },
1312
+ {
1313
+ slot: "sidebar.footer.action",
1314
+ options: {
1315
+ id: "routerai-balance",
1316
+ inject: shared
1317
+ },
1318
+ component: SidebarBalance
1319
+ },
1320
+ {
1321
+ slot: "conversation.composer.dock",
1322
+ options: {
1323
+ id: "routerai-session-cost",
1324
+ order: 5,
1325
+ inject: (sessionId) => ({
1326
+ remote,
1327
+ sessionId
1328
+ })
1329
+ },
1330
+ component: SessionCost
1331
+ },
1332
+ {
1333
+ slot: "shell.quota-notice",
1334
+ options: {
1335
+ select: (owner) => owner.code === "QUOTA" && remote.read().overview?.problem != null ? owner : null,
1336
+ inject: shared
1337
+ },
1338
+ component: MoneyNotice
1339
+ }
1340
+ ];
1341
+ for (const { slot, options, component } of placements) slots.inject(slot, () => slots.register({
1342
+ name: slot,
1343
+ locale: NAMESPACE,
1344
+ ...options
1345
+ }, component));
1346
+ }
1347
+ exports.apply = apply;
1348
+ exports.inject = inject;
1349
+ exports.name = name;
1350
+ return module.exports;
1351
+ }
1352
+ });