dsh-plugin-subscription-usage 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +50 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +9 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +233 -0
- package/lib/auth/rpc.js +501 -0
- package/lib/auth/store.d.ts +198 -0
- package/lib/auth/store.js +473 -0
- package/lib/client/Footer.d.ts +3 -0
- package/lib/client/Footer.js +73 -0
- package/lib/client/Gauge.d.ts +28 -0
- package/lib/client/Gauge.js +139 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +130 -0
- package/lib/client/Notice.d.ts +15 -0
- package/lib/client/Notice.js +93 -0
- package/lib/client/ProviderCard.d.ts +35 -0
- package/lib/client/ProviderCard.js +240 -0
- package/lib/client/SpeedSelect.d.ts +72 -0
- package/lib/client/SpeedSelect.js +214 -0
- package/lib/client/Usage.d.ts +7 -0
- package/lib/client/Usage.js +83 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +172 -0
- package/lib/client/context.d.ts +16 -0
- package/lib/client/context.js +11 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/hooks.d.ts +2 -0
- package/lib/client/hooks.js +10 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client/index.js +113 -0
- package/lib/client/locales.d.ts +139 -0
- package/lib/client/locales.js +267 -0
- package/lib/client/meter.d.ts +91 -0
- package/lib/client/meter.js +196 -0
- package/lib/client/prefs.d.ts +51 -0
- package/lib/client/prefs.js +71 -0
- package/lib/client/rpc.d.ts +161 -0
- package/lib/client/rpc.js +31 -0
- package/lib/client/store.d.ts +67 -0
- package/lib/client/store.js +129 -0
- package/lib/client/styles.d.ts +7 -0
- package/lib/client/styles.js +176 -0
- package/lib/client.js +3933 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +55 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +134 -0
- package/lib/http.js +431 -0
- package/lib/index.d.ts +121 -0
- package/lib/index.js +13399 -0
- package/lib/json-text.d.ts +2 -0
- package/lib/json-text.js +4 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +238 -0
- package/lib/model-speeds.d.ts +10 -0
- package/lib/model-speeds.js +48 -0
- package/lib/models-directory.d.ts +17 -0
- package/lib/models-directory.js +25 -0
- package/lib/provider-settings.d.ts +55 -0
- package/lib/provider-settings.js +215 -0
- package/lib/providers/account-preferences.d.ts +46 -0
- package/lib/providers/account-preferences.js +188 -0
- package/lib/providers/accounts.d.ts +113 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +119 -0
- package/lib/providers/antigravity.js +552 -0
- package/lib/providers/catalog-refresh.d.ts +4 -0
- package/lib/providers/catalog-refresh.js +30 -0
- package/lib/providers/catalog-store.d.ts +47 -0
- package/lib/providers/catalog-store.js +283 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +212 -0
- package/lib/providers/claude.js +726 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +253 -0
- package/lib/providers/codex.js +930 -0
- package/lib/providers/common.d.ts +415 -0
- package/lib/providers/common.js +679 -0
- package/lib/providers/copilot.d.ts +365 -0
- package/lib/providers/copilot.js +956 -0
- package/lib/providers/grok.d.ts +214 -0
- package/lib/providers/grok.js +750 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +91 -0
- package/lib/providers/pool-health.js +164 -0
- package/lib/providers/pool-usage.d.ts +106 -0
- package/lib/providers/pool-usage.js +302 -0
- package/lib/providers/pool.d.ts +115 -0
- package/lib/providers/pool.js +461 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +37 -0
- package/lib/providers/reset-redemption.d.ts +20 -0
- package/lib/providers/reset-redemption.js +70 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +16 -0
- package/lib/tools/registration.js +25 -0
- package/lib/tools/usage.d.ts +62 -0
- package/lib/tools/usage.js +89 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +180 -0
- package/lib/translate/anthropic.js +534 -0
- package/lib/translate/antigravity-images.d.ts +11 -0
- package/lib/translate/antigravity-images.js +62 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +120 -0
- package/lib/translate/antigravity.js +412 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +98 -0
- package/lib/translate/resolved.js +186 -0
- package/lib/translate/responses.d.ts +192 -0
- package/lib/translate/responses.js +443 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +62 -0
- package/lib/usage/headers.d.ts +21 -0
- package/lib/usage/headers.js +64 -0
- package/lib/usage/pace.d.ts +53 -0
- package/lib/usage/pace.js +48 -0
- package/lib/usage/service.d.ts +64 -0
- package/lib/usage/service.js +104 -0
- package/lib/usage-providers/common.d.ts +23 -0
- package/lib/usage-providers/common.js +28 -0
- package/lib/usage-providers/index.d.ts +42 -0
- package/lib/usage-providers/index.js +71 -0
- package/lib/usage-providers/keys.d.ts +11 -0
- package/lib/usage-providers/keys.js +52 -0
- package/lib/usage-providers/kimi.d.ts +4 -0
- package/lib/usage-providers/kimi.js +52 -0
- package/lib/usage-providers/ollama-cloud.d.ts +4 -0
- package/lib/usage-providers/ollama-cloud.js +42 -0
- package/lib/usage-providers/zai.d.ts +5 -0
- package/lib/usage-providers/zai.js +53 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +115 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,3933 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-plugin-subscription-usage", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
//#region rolldown:runtime
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
21
|
+
value: mod,
|
|
22
|
+
enumerable: true
|
|
23
|
+
}) : target, mod));
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
let react = require("react");
|
|
27
|
+
react = __toESM(react);
|
|
28
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
30
|
+
let react_dom = require("react-dom");
|
|
31
|
+
react_dom = __toESM(react_dom);
|
|
32
|
+
|
|
33
|
+
//#region src/client/locales.ts
|
|
34
|
+
/** Copy for every Client surface of this plugin, in one locale namespace. */
|
|
35
|
+
const en = {
|
|
36
|
+
signIn: "Sign in",
|
|
37
|
+
signInAgain: "Sign in",
|
|
38
|
+
noApiKey: "No API key needed",
|
|
39
|
+
signInExpired: "Sign-in expired",
|
|
40
|
+
signedIn: "Signed in",
|
|
41
|
+
signInNeeded: "Sign-in needed",
|
|
42
|
+
manage: "Manage",
|
|
43
|
+
modelsCount: "{visible}/{total} models",
|
|
44
|
+
modelsCountTitle: "Models shown in the picker",
|
|
45
|
+
refreshUsage: "Refresh usage",
|
|
46
|
+
refreshUsageNamed: "Refresh {name} usage",
|
|
47
|
+
refreshedWait: "Refreshed. Again in {seconds}s",
|
|
48
|
+
defaultAccount: "Default account",
|
|
49
|
+
servingAccount: "Serving this session",
|
|
50
|
+
live: "live",
|
|
51
|
+
liveTitle: "Updates with every response",
|
|
52
|
+
agoShort: "{time} ago",
|
|
53
|
+
staleShort: "{time} old",
|
|
54
|
+
staleTitle: "Refresh failed. Retrying {when}",
|
|
55
|
+
noUsageData: "No usage data",
|
|
56
|
+
noUsageDataTitle: "This provider does not publish usage",
|
|
57
|
+
usageUnavailable: "Usage unavailable",
|
|
58
|
+
moreAccounts: "{count} more accounts",
|
|
59
|
+
stepTabOpened: "Tab opened",
|
|
60
|
+
stepApprove: "Approve access",
|
|
61
|
+
stepLoadModels: "Load models",
|
|
62
|
+
stepCodeIssued: "Code issued",
|
|
63
|
+
stepWaitingGithub: "Waiting for GitHub",
|
|
64
|
+
pasteRedirect: "Paste redirect URL instead",
|
|
65
|
+
pastePlaceholder: "Redirect URL or code",
|
|
66
|
+
continue: "Continue",
|
|
67
|
+
cancel: "Cancel",
|
|
68
|
+
copyCode: "Copy code",
|
|
69
|
+
claudeImport: "Import Claude Code",
|
|
70
|
+
claudeBrowser: "Browser sign-in",
|
|
71
|
+
tabAccounts: "Accounts",
|
|
72
|
+
tabModels: "Models",
|
|
73
|
+
tabTools: "Tools",
|
|
74
|
+
pool: "Pool",
|
|
75
|
+
poolInclude: "Include {account} in the pool",
|
|
76
|
+
makeDefault: "Make default",
|
|
77
|
+
signOut: "Sign out",
|
|
78
|
+
addAccount: "Add account",
|
|
79
|
+
searchModels: "Search {count} models",
|
|
80
|
+
showInPicker: "Show {model} in the picker",
|
|
81
|
+
pickerCount: "{visible} of {total} in the picker",
|
|
82
|
+
autoShowNew: "Show new models automatically",
|
|
83
|
+
selectAll: "Select all",
|
|
84
|
+
deselectAll: "Deselect all",
|
|
85
|
+
effortFor: "Default effort for {model}",
|
|
86
|
+
effortAuto: "Auto",
|
|
87
|
+
toolWebSearch: "Web search",
|
|
88
|
+
toolImage: "Image generation",
|
|
89
|
+
toolVideo: "Video generation",
|
|
90
|
+
toolXSearch: "X search",
|
|
91
|
+
toolsNote: "Applies to new sessions",
|
|
92
|
+
save: "Save",
|
|
93
|
+
saving: "Saving…",
|
|
94
|
+
saveFailed: "Could not save: {message}",
|
|
95
|
+
loadFailed: "Could not load: {message}",
|
|
96
|
+
window5h: "5h",
|
|
97
|
+
window7d: "7d",
|
|
98
|
+
windowOther: "limit",
|
|
99
|
+
resetsIn: "in {time}",
|
|
100
|
+
emptyAt: "empty {time}",
|
|
101
|
+
backAt: "back {time}",
|
|
102
|
+
tipUsedLeft: "{used}% used · {left}% left",
|
|
103
|
+
tipElapsed: "{percent}% of the window has passed",
|
|
104
|
+
tipPace: "Pace: ~{percent}% by reset",
|
|
105
|
+
tipResets: "Resets {time}",
|
|
106
|
+
tipCount: "{used} of {limit} used",
|
|
107
|
+
notReported: "Not reported",
|
|
108
|
+
subscriptions: "Subscriptions",
|
|
109
|
+
addProvider: "Add a provider",
|
|
110
|
+
apiKey: "API key",
|
|
111
|
+
addKey: "Add",
|
|
112
|
+
removeKey: "Remove key",
|
|
113
|
+
keyHelp: "Paste an API key. It is checked once, then stored on this machine.",
|
|
114
|
+
keyChecking: "Checking key…",
|
|
115
|
+
usageEmpty: "Sign in to a subscription in Models to see its usage here.",
|
|
116
|
+
composerUsage: "Composer usage",
|
|
117
|
+
composerUsageNote: "Current model plus pinned",
|
|
118
|
+
showQuotaAs: "Show quota as",
|
|
119
|
+
displayLeft: "Left",
|
|
120
|
+
displayUsed: "Used",
|
|
121
|
+
paceWarnings: "Pace warnings",
|
|
122
|
+
proxy: "Proxy",
|
|
123
|
+
proxyOff: "Off",
|
|
124
|
+
proxyOn: "On",
|
|
125
|
+
configure: "Configure",
|
|
126
|
+
proxyUrl: "Proxy URL",
|
|
127
|
+
proxyBypass: "Bypass hosts, comma separated",
|
|
128
|
+
proxyEnabled: "Use this proxy",
|
|
129
|
+
proxyTest: "Test",
|
|
130
|
+
proxyTestOk: "HTTP {status} in {ms} ms",
|
|
131
|
+
usage: "Usage",
|
|
132
|
+
usageLabel: "Subscription usage",
|
|
133
|
+
refreshNow: "Refresh now",
|
|
134
|
+
usageSettings: "Usage settings",
|
|
135
|
+
pin: "Pin to composer",
|
|
136
|
+
unpin: "Unpin from composer",
|
|
137
|
+
otherProviders: "{count} more",
|
|
138
|
+
noticeEmpty: "{provider} {window} empty around {time}",
|
|
139
|
+
noticeOut: "{provider} {window} limit reached",
|
|
140
|
+
noticeSwitched: "{provider} limit reached. Switched to {account}",
|
|
141
|
+
dismiss: "Dismiss",
|
|
142
|
+
generating: "Generating image…",
|
|
143
|
+
generatingVideo: "Generating video…",
|
|
144
|
+
image: "image",
|
|
145
|
+
imageClose: "Close",
|
|
146
|
+
imageLoadFailed: "Retry",
|
|
147
|
+
imageLoading: "Loading…",
|
|
148
|
+
imagePreview: "Image preview",
|
|
149
|
+
speed: "Speed",
|
|
150
|
+
speedStandard: "Standard",
|
|
151
|
+
speedStandardDescription: "Default speed",
|
|
152
|
+
speedFast: "Fast",
|
|
153
|
+
speedFastDescription: "1.5x speed, more usage",
|
|
154
|
+
videoLoadFailed: "Video failed to load: {message}",
|
|
155
|
+
videoLoading: "Loading video…",
|
|
156
|
+
viewImage: "View image",
|
|
157
|
+
viewImageNamed: "View {name}",
|
|
158
|
+
commandFast: "Switch the Codex speed tier (Standard/Fast)",
|
|
159
|
+
commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one"
|
|
160
|
+
};
|
|
161
|
+
/** The locale namespace this plugin registers. */
|
|
162
|
+
const LOCALE_NS = "subscription-models";
|
|
163
|
+
const zh = {
|
|
164
|
+
signIn: "登录",
|
|
165
|
+
signInAgain: "重新登录",
|
|
166
|
+
noApiKey: "无需 API 密钥",
|
|
167
|
+
signInExpired: "登录已过期",
|
|
168
|
+
signedIn: "已登录",
|
|
169
|
+
signInNeeded: "需要登录",
|
|
170
|
+
manage: "管理",
|
|
171
|
+
modelsCount: "{visible}/{total} 个模型",
|
|
172
|
+
modelsCountTitle: "模型选择器中显示的模型",
|
|
173
|
+
refreshUsage: "刷新用量",
|
|
174
|
+
refreshUsageNamed: "刷新 {name} 用量",
|
|
175
|
+
refreshedWait: "已刷新,{seconds} 秒后可再次刷新",
|
|
176
|
+
defaultAccount: "默认账号",
|
|
177
|
+
servingAccount: "正在为本会话服务",
|
|
178
|
+
live: "实时",
|
|
179
|
+
liveTitle: "每次回复后更新",
|
|
180
|
+
agoShort: "{time}前",
|
|
181
|
+
staleShort: "已过 {time}",
|
|
182
|
+
staleTitle: "刷新失败,{when}重试",
|
|
183
|
+
noUsageData: "无用量数据",
|
|
184
|
+
noUsageDataTitle: "该服务商不提供用量数据",
|
|
185
|
+
usageUnavailable: "用量不可用",
|
|
186
|
+
moreAccounts: "另外 {count} 个账号",
|
|
187
|
+
stepTabOpened: "已打开标签页",
|
|
188
|
+
stepApprove: "批准访问",
|
|
189
|
+
stepLoadModels: "加载模型",
|
|
190
|
+
stepCodeIssued: "已生成代码",
|
|
191
|
+
stepWaitingGithub: "等待 GitHub",
|
|
192
|
+
pasteRedirect: "改为粘贴回调 URL",
|
|
193
|
+
pastePlaceholder: "回调 URL 或授权码",
|
|
194
|
+
continue: "继续",
|
|
195
|
+
cancel: "取消",
|
|
196
|
+
copyCode: "复制代码",
|
|
197
|
+
claudeImport: "导入 Claude Code",
|
|
198
|
+
claudeBrowser: "浏览器登录",
|
|
199
|
+
tabAccounts: "账号",
|
|
200
|
+
tabModels: "模型",
|
|
201
|
+
tabTools: "工具",
|
|
202
|
+
pool: "账号池",
|
|
203
|
+
poolInclude: "将 {account} 加入账号池",
|
|
204
|
+
makeDefault: "设为默认",
|
|
205
|
+
signOut: "退出登录",
|
|
206
|
+
addAccount: "添加账号",
|
|
207
|
+
searchModels: "搜索 {count} 个模型",
|
|
208
|
+
showInPicker: "在选择器中显示 {model}",
|
|
209
|
+
pickerCount: "选择器中显示 {visible}/{total}",
|
|
210
|
+
autoShowNew: "自动显示新模型",
|
|
211
|
+
selectAll: "全选",
|
|
212
|
+
deselectAll: "全不选",
|
|
213
|
+
effortFor: "{model} 的默认推理强度",
|
|
214
|
+
effortAuto: "自动",
|
|
215
|
+
toolWebSearch: "网页搜索",
|
|
216
|
+
toolImage: "图片生成",
|
|
217
|
+
toolVideo: "视频生成",
|
|
218
|
+
toolXSearch: "X 搜索",
|
|
219
|
+
toolsNote: "对新会话生效",
|
|
220
|
+
save: "保存",
|
|
221
|
+
saving: "保存中…",
|
|
222
|
+
saveFailed: "保存失败:{message}",
|
|
223
|
+
loadFailed: "加载失败:{message}",
|
|
224
|
+
window5h: "5h",
|
|
225
|
+
window7d: "7d",
|
|
226
|
+
windowOther: "限额",
|
|
227
|
+
resetsIn: "{time}后",
|
|
228
|
+
emptyAt: "{time} 用尽",
|
|
229
|
+
backAt: "{time} 恢复",
|
|
230
|
+
tipUsedLeft: "已用 {used}% · 剩余 {left}%",
|
|
231
|
+
tipElapsed: "窗口已过 {percent}%",
|
|
232
|
+
tipPace: "按当前速度:重置前约用 {percent}%",
|
|
233
|
+
tipResets: "{time} 重置",
|
|
234
|
+
tipCount: "已用 {used} / {limit}",
|
|
235
|
+
notReported: "未提供",
|
|
236
|
+
subscriptions: "订阅",
|
|
237
|
+
addProvider: "添加提供商",
|
|
238
|
+
apiKey: "API 密钥",
|
|
239
|
+
addKey: "添加",
|
|
240
|
+
removeKey: "移除密钥",
|
|
241
|
+
keyHelp: "粘贴 API 密钥。验证一次后保存在本机。",
|
|
242
|
+
keyChecking: "正在验证密钥…",
|
|
243
|
+
usageEmpty: "在“模型”中登录订阅后,可在此查看用量。",
|
|
244
|
+
composerUsage: "输入框用量",
|
|
245
|
+
composerUsageNote: "当前模型和已固定",
|
|
246
|
+
showQuotaAs: "额度显示为",
|
|
247
|
+
displayLeft: "剩余",
|
|
248
|
+
displayUsed: "已用",
|
|
249
|
+
paceWarnings: "速度预警",
|
|
250
|
+
proxy: "代理",
|
|
251
|
+
proxyOff: "关闭",
|
|
252
|
+
proxyOn: "开启",
|
|
253
|
+
configure: "配置",
|
|
254
|
+
proxyUrl: "代理 URL",
|
|
255
|
+
proxyBypass: "绕过的主机,逗号分隔",
|
|
256
|
+
proxyEnabled: "使用此代理",
|
|
257
|
+
proxyTest: "测试",
|
|
258
|
+
proxyTestOk: "HTTP {status},耗时 {ms} ms",
|
|
259
|
+
usage: "用量",
|
|
260
|
+
usageLabel: "订阅用量",
|
|
261
|
+
refreshNow: "立即刷新",
|
|
262
|
+
usageSettings: "用量设置",
|
|
263
|
+
pin: "固定到输入框",
|
|
264
|
+
unpin: "从输入框取消固定",
|
|
265
|
+
otherProviders: "另外 {count} 个",
|
|
266
|
+
noticeEmpty: "{provider} {window} 预计 {time} 用尽",
|
|
267
|
+
noticeOut: "{provider} {window} 已达上限",
|
|
268
|
+
noticeSwitched: "{provider} 已达上限,已切换到 {account}",
|
|
269
|
+
dismiss: "关闭",
|
|
270
|
+
generating: "正在生成图片…",
|
|
271
|
+
generatingVideo: "正在生成视频…",
|
|
272
|
+
image: "图片",
|
|
273
|
+
imageClose: "关闭",
|
|
274
|
+
imageLoadFailed: "重试",
|
|
275
|
+
imageLoading: "加载中…",
|
|
276
|
+
imagePreview: "图片预览",
|
|
277
|
+
speed: "速度",
|
|
278
|
+
speedStandard: "标准",
|
|
279
|
+
speedStandardDescription: "默认速度",
|
|
280
|
+
speedFast: "快速",
|
|
281
|
+
speedFastDescription: "约 1.5 倍速度,消耗更多用量",
|
|
282
|
+
videoLoadFailed: "视频加载失败:{message}",
|
|
283
|
+
videoLoading: "视频加载中…",
|
|
284
|
+
viewImage: "查看图片",
|
|
285
|
+
viewImageNamed: "查看 {name}",
|
|
286
|
+
commandFast: "切换 Codex 速度档(标准/快速)",
|
|
287
|
+
commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
|
|
288
|
+
};
|
|
289
|
+
/** Fill `{name}` placeholders. */
|
|
290
|
+
function format(text, params = {}) {
|
|
291
|
+
return text.replace(/\{(\w+)\}/g, (match, key) => key in params ? String(params[key]) : match);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/client/styles.ts
|
|
296
|
+
/**
|
|
297
|
+
* One stylesheet for every surface, prefixed `dsm-`, colored only through
|
|
298
|
+
* `--dsw-alias-*` tokens. Card and button values copy the Models page
|
|
299
|
+
* (`dsh-client-ui-settings-models` 0.2.2-rc.1); meters follow
|
|
300
|
+
* design/usage-draft.html v3.
|
|
301
|
+
*/
|
|
302
|
+
const css = `
|
|
303
|
+
.dsm-sub { display: flex; flex-direction: column; gap: 8px; min-width: 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
|
|
304
|
+
.dsm-sub .dsm-card-error { color: var(--dsw-alias-state-error-primary); font-size: 12px; }
|
|
305
|
+
.dsm-num, .dsm-w, .dsm-mini, .dsm-fresh { font-variant-numeric: tabular-nums; }
|
|
306
|
+
|
|
307
|
+
.dsm-btn { box-sizing: border-box; height: 28px; padding: 0 10px; border-radius: var(--dsw-radius-sm); font: inherit; font-size: 12px; line-height: 18px;
|
|
308
|
+
cursor: pointer; border: .5px solid var(--dsw-alias-border-l3); background: transparent; color: var(--dsw-alias-label-primary);
|
|
309
|
+
display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
|
310
|
+
.dsm-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid, var(--dsw-alias-interactive-bg-hover)); }
|
|
311
|
+
.dsm-btn:disabled { opacity: .4; cursor: default; }
|
|
312
|
+
.dsm-btn:focus-visible, .dsm-fold:focus-visible, .dsm-seg button:focus-visible, .dsm-switch:focus-visible, .dsm-chip:focus-visible, .dsm-gauge:focus-visible, .dsm-tab:focus-visible {
|
|
313
|
+
box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline: none; }
|
|
314
|
+
.dsm-btn.dsm-primary { background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary)); color: var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-base)); border-color: transparent; }
|
|
315
|
+
.dsm-btn.dsm-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-label-primary)); }
|
|
316
|
+
.dsm-btn.dsm-ghost { border-color: transparent; color: var(--dsw-alias-label-secondary); }
|
|
317
|
+
.dsm-btn.dsm-danger { border-color: transparent; color: var(--dsw-alias-state-error-primary); }
|
|
318
|
+
.dsm-btn.dsm-danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger, var(--dsw-alias-interactive-bg-hover)); }
|
|
319
|
+
.dsm-btn.dsm-icon { width: 28px; padding: 0; justify-content: center; border-color: transparent; color: var(--dsw-alias-label-secondary); }
|
|
320
|
+
.dsm-btn.dsm-icon.dsm-sm { width: 24px; height: 24px; }
|
|
321
|
+
.dsm-spinning svg { animation: dsm-spin .7s linear infinite; }
|
|
322
|
+
@keyframes dsm-spin { to { transform: rotate(360deg); } }
|
|
323
|
+
|
|
324
|
+
.dsm-acct { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
325
|
+
.dsm-acct-head { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--dsw-alias-label-secondary); }
|
|
326
|
+
.dsm-acct-name { color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
327
|
+
.dsm-tag { flex: none; font-size: 11px; line-height: 16px; padding: 0 5px; border-radius: var(--dsw-radius-xs); border: .5px solid var(--dsw-alias-border-l3); color: var(--dsw-alias-label-secondary); }
|
|
328
|
+
.dsm-glyph { display: inline-flex; flex: none; color: var(--dsw-alias-label-tertiary); }
|
|
329
|
+
.dsm-glyph.dsm-star { color: var(--dsw-alias-state-warn-primary); }
|
|
330
|
+
.dsm-glyph.dsm-live { color: var(--dsw-alias-state-business-primary); }
|
|
331
|
+
.dsm-fresh { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--dsw-alias-label-tertiary); white-space: nowrap; flex: none; }
|
|
332
|
+
.dsm-fresh.dsm-stale { color: var(--dsw-alias-state-warn-primary); }
|
|
333
|
+
.dsm-live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-success-primary); }
|
|
334
|
+
|
|
335
|
+
/* Fixed label, value and time columns, so bars line up across accounts and providers. */
|
|
336
|
+
.dsm-wins { display: grid; grid-template-columns: fit-content(132px) minmax(48px, 1fr) 40px 92px; column-gap: 12px; row-gap: 6px; align-items: center; }
|
|
337
|
+
.dsm-w { display: contents; font-size: 12px; line-height: 18px; }
|
|
338
|
+
.dsm-wl { color: var(--dsw-alias-label-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; line-height: 18px; }
|
|
339
|
+
.dsm-wv { text-align: right; font-weight: 600; font-size: 13px; line-height: 18px; color: var(--dsw-alias-label-primary); }
|
|
340
|
+
.dsm-wt { text-align: right; color: var(--dsw-alias-label-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; line-height: 18px; }
|
|
341
|
+
.dsm-w.dsm-tight .dsm-wv, .dsm-w.dsm-tight .dsm-wt { color: var(--dsw-alias-state-warn-primary); }
|
|
342
|
+
.dsm-w.dsm-over .dsm-wv, .dsm-w.dsm-over .dsm-wt, .dsm-w.dsm-out .dsm-wv, .dsm-w.dsm-out .dsm-wt { color: var(--dsw-alias-state-error-primary); }
|
|
343
|
+
|
|
344
|
+
.dsm-bar { position: relative; display: block; border-radius: 3px; background: color-mix(in srgb, var(--dsw-alias-label-primary) 9%, transparent); color: var(--dsw-alias-state-success-primary); }
|
|
345
|
+
.dsm-fill { position: absolute; top: 0; bottom: 0; left: 0; border-radius: inherit; background: currentColor; transition: width .5s cubic-bezier(.16, 1, .3, 1); }
|
|
346
|
+
.dsm-proj { position: absolute; top: 0; bottom: 0; background: repeating-linear-gradient(90deg, transparent 0 2px, color-mix(in srgb, currentColor 55%, transparent) 2px 3px); }
|
|
347
|
+
.dsm-bar.dsm-tight { color: var(--dsw-alias-state-warn-primary); }
|
|
348
|
+
.dsm-bar.dsm-over, .dsm-bar.dsm-out { color: var(--dsw-alias-state-error-primary); }
|
|
349
|
+
.dsm-bar.dsm-unknown .dsm-fill { display: none; }
|
|
350
|
+
|
|
351
|
+
.dsm-minis { display: inline-flex; gap: 14px; flex: none; }
|
|
352
|
+
.dsm-mini { display: inline-grid; grid-template-columns: auto 32px 34px; align-items: center; gap: 5px; font-size: 11px; line-height: 16px; }
|
|
353
|
+
.dsm-mini i { font-style: normal; color: var(--dsw-alias-label-tertiary); max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
354
|
+
.dsm-mini b { font-weight: 500; text-align: right; color: var(--dsw-alias-label-primary); font-size: 12px; }
|
|
355
|
+
.dsm-mini.dsm-tight b { color: var(--dsw-alias-state-warn-primary); }
|
|
356
|
+
.dsm-mini.dsm-over b, .dsm-mini.dsm-out b { color: var(--dsw-alias-state-error-primary); }
|
|
357
|
+
|
|
358
|
+
.dsm-fold { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; width: 100%; min-height: 32px; padding: 0 6px 0 2px;
|
|
359
|
+
border: 0; background: none; text-align: left; cursor: pointer; border-radius: var(--dsw-radius-md); font: inherit; font-size: 12px; color: var(--dsw-alias-label-secondary); }
|
|
360
|
+
.dsm-fold:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
361
|
+
.dsm-fold .dsm-chev { color: var(--dsw-alias-label-tertiary); transition: transform .2s cubic-bezier(.16, 1, .3, 1); }
|
|
362
|
+
.dsm-fold[aria-expanded="true"] .dsm-chev { transform: rotate(90deg); }
|
|
363
|
+
.dsm-fold-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
364
|
+
.dsm-fold-name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
365
|
+
.dsm-fold-body { display: flex; flex-direction: column; gap: 8px; padding: 2px 0 4px 24px; min-width: 0; }
|
|
366
|
+
|
|
367
|
+
.dsm-foot { display: flex; align-items: center; gap: 6px; }
|
|
368
|
+
.dsm-muted { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; margin: 0; display: inline-flex; align-items: center; gap: 6px; }
|
|
369
|
+
.dsm-alert { display: flex; gap: 8px; align-items: center; padding: 6px 6px 6px 10px; border-radius: var(--dsw-radius-lg);
|
|
370
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); }
|
|
371
|
+
.dsm-alert > svg { color: var(--dsw-alias-state-error-primary); }
|
|
372
|
+
.dsm-alert .dsm-btn { margin-left: auto; }
|
|
373
|
+
|
|
374
|
+
.dsm-panel { border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-layer-2)); padding: 2px 12px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
375
|
+
.dsm-tabs { display: flex; gap: 2px; border-bottom: .5px solid var(--dsw-alias-border-l2); margin: 0 -12px; padding: 0 8px; }
|
|
376
|
+
.dsm-tab { border: 0; background: none; padding: 9px 8px 8px; font: inherit; font-size: 13px; color: var(--dsw-alias-label-tertiary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -.5px; }
|
|
377
|
+
.dsm-tab[aria-selected="true"] { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-label-primary); }
|
|
378
|
+
.dsm-row { display: flex; align-items: center; gap: 10px; min-height: 36px; font-size: 13px; }
|
|
379
|
+
.dsm-row + .dsm-row { border-top: .5px solid var(--dsw-alias-border-l1); }
|
|
380
|
+
.dsm-grow { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; }
|
|
381
|
+
.dsm-grow > .dsm-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
382
|
+
.dsm-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
383
|
+
.dsm-list { max-height: 280px; overflow-y: auto; }
|
|
384
|
+
.dsm-switch { width: 30px; height: 18px; border-radius: 9px; background: var(--dsw-alias-state-idle-primary); border: 0; position: relative; cursor: pointer; flex: none; padding: 0; }
|
|
385
|
+
.dsm-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .2s cubic-bezier(.16, 1, .3, 1); }
|
|
386
|
+
.dsm-switch[aria-checked="true"] { background: var(--dsw-alias-state-success-primary); }
|
|
387
|
+
.dsm-switch[aria-checked="true"]::after { transform: translateX(12px); }
|
|
388
|
+
.dsm-switch:disabled { opacity: .4; cursor: default; }
|
|
389
|
+
.dsm-input { box-sizing: border-box; height: 30px; width: 100%; border-radius: var(--dsw-radius-md); border: .5px solid var(--dsw-alias-border-l3);
|
|
390
|
+
background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); padding: 0 10px; font: inherit; font-size: 13px; }
|
|
391
|
+
.dsm-select { height: 26px; border-radius: var(--dsw-radius-sm); border: .5px solid var(--dsw-alias-border-l3); background: var(--dsw-alias-bg-layer-1);
|
|
392
|
+
color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 0 4px; max-width: 160px; }
|
|
393
|
+
.dsm-panel-foot { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
394
|
+
.dsm-panel-foot .dsm-card-error { margin-right: auto; }
|
|
395
|
+
.dsm-seg { display: inline-flex; padding: 2px; border-radius: 7px; background: color-mix(in srgb, var(--dsw-alias-label-primary) 9%, transparent); gap: 2px; flex: none; }
|
|
396
|
+
.dsm-seg button { border: 0; background: transparent; height: 22px; padding: 0 9px; border-radius: 5px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
397
|
+
.dsm-seg button[aria-pressed="true"] { background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); box-shadow: 0 1px 2px #0000002e; }
|
|
398
|
+
.dsm-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
399
|
+
.dsm-chip { height: 26px; padding: 0 10px 0 8px; border-radius: 13px; border: .5px solid var(--dsw-alias-border-l3); background: transparent; font: inherit; font-size: 12px;
|
|
400
|
+
cursor: pointer; display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-secondary); }
|
|
401
|
+
.dsm-chip[aria-pressed="true"] { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); border-color: transparent; }
|
|
402
|
+
.dsm-chip:disabled { opacity: .4; cursor: default; }
|
|
403
|
+
|
|
404
|
+
.dsm-signin { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-layer-2)); }
|
|
405
|
+
.dsm-steps { display: flex; gap: 16px; margin: 0; padding: 0; list-style: none; font-size: 12px; flex-wrap: wrap; }
|
|
406
|
+
.dsm-steps li { display: inline-flex; gap: 6px; align-items: center; color: var(--dsw-alias-label-tertiary); }
|
|
407
|
+
.dsm-steps li[data-s="done"] { color: var(--dsw-alias-state-success-primary); }
|
|
408
|
+
.dsm-steps li[data-s="now"] { color: var(--dsw-alias-label-primary); }
|
|
409
|
+
.dsm-spin { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--dsw-alias-border-l3); border-top-color: currentColor; animation: dsm-spin .8s linear infinite; flex: none; box-sizing: border-box; }
|
|
410
|
+
.dsm-code { font: 600 20px/28px var(--dsw-font-family-mono, ui-monospace, Menlo, monospace); letter-spacing: .12em; padding: 4px 12px; border-radius: var(--dsw-radius-md);
|
|
411
|
+
background: var(--dsw-alias-bg-layer-1); border: .5px solid var(--dsw-alias-border-l2); }
|
|
412
|
+
.dsm-fallback summary { cursor: pointer; color: var(--dsw-alias-label-tertiary); font-size: 12px; list-style: none; display: inline-flex; gap: 4px; align-items: center; }
|
|
413
|
+
.dsm-fallback summary::-webkit-details-marker { display: none; }
|
|
414
|
+
.dsm-fallback[open] summary { margin-bottom: 6px; }
|
|
415
|
+
|
|
416
|
+
.dsm-footer-block { margin-top: 18px; padding-top: 14px; border-top: .5px solid var(--dsw-alias-border-l2); display: flex; flex-direction: column; max-width: 720px; }
|
|
417
|
+
.dsm-footer-block h3 { margin: 0 0 4px; font-size: 14px; font-weight: 500; line-height: 22px; color: var(--dsw-alias-label-primary); }
|
|
418
|
+
.dsm-footer-block .dsm-row { min-height: 40px; color: var(--dsw-alias-label-primary); }
|
|
419
|
+
|
|
420
|
+
.dsm-gauge-slot { position: relative; display: inline-flex; }
|
|
421
|
+
.dsm-gauge { display: inline-flex; align-items: center; gap: 10px; height: 24px; padding: 0 8px; border-radius: 12px; border: 0; background: transparent; cursor: pointer;
|
|
422
|
+
color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; }
|
|
423
|
+
.dsm-gauge:hover, .dsm-gauge[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-hover); }
|
|
424
|
+
.dsm-gp { display: inline-flex; align-items: center; gap: 8px; }
|
|
425
|
+
.dsm-gp + .dsm-gp { padding-left: 10px; border-left: .5px solid var(--dsw-alias-border-l3); }
|
|
426
|
+
.dsm-gp-name { color: var(--dsw-alias-label-primary); }
|
|
427
|
+
.dsm-gp .dsm-minis { gap: 10px; }
|
|
428
|
+
.dsm-gp .dsm-mini { grid-template-columns: auto 26px auto; }
|
|
429
|
+
|
|
430
|
+
.dsm-pop { position: fixed; z-index: 1000; width: 420px; max-width: calc(100vw - 20px); box-sizing: border-box; background: var(--dsw-specific-menu, var(--dsw-alias-bg-overlay));
|
|
431
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, none); border-radius: var(--dsw-radius-xl); box-shadow: var(--dsw-elevation-panel, 0 12px 32px #0006);
|
|
432
|
+
border: .5px solid var(--dsw-alias-border-l2); padding: 6px; display: flex; flex-direction: column; color: var(--dsw-alias-label-primary);
|
|
433
|
+
text-align: left; font-size: 13px; line-height: 20px; max-height: min(560px, calc(100vh - 24px)); overflow: hidden auto; }
|
|
434
|
+
.dsm-pop-head { display: flex; align-items: center; gap: 2px; padding: 2px 2px 6px 8px; }
|
|
435
|
+
.dsm-pop-head b { font-weight: 600; font-size: 13px; margin-right: auto; }
|
|
436
|
+
.dsm-psec { border-top: .5px solid var(--dsw-alias-border-l2); padding: 4px 0; }
|
|
437
|
+
.dsm-psec-head { display: flex; align-items: center; gap: 2px; }
|
|
438
|
+
.dsm-psec-head .dsm-fold { flex: 1; }
|
|
439
|
+
.dsm-psec-name { color: var(--dsw-alias-label-primary); font-weight: 500; font-size: 13px; }
|
|
440
|
+
.dsm-psec .dsm-fold-body { padding: 0 8px 6px 26px; gap: 6px; }
|
|
441
|
+
.dsm-psec .dsm-fold-body .dsm-fold-body { padding: 4px 0 0; }
|
|
442
|
+
.dsm-psec .dsm-fold-body .dsm-fold { margin-left: -24px; width: calc(100% + 24px); }
|
|
443
|
+
.dsm-pacct { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
444
|
+
.dsm-pacct .dsm-acct-head { font-size: 12px; line-height: 18px; }
|
|
445
|
+
.dsm-others { padding-bottom: 0; }
|
|
446
|
+
.dsm-others .dsm-psec { border-top: 0; padding: 0 0 0 16px; }
|
|
447
|
+
.dsm-gauge > svg { color: var(--dsw-alias-label-secondary); }
|
|
448
|
+
.dsm-pin { color: var(--dsw-alias-label-tertiary); }
|
|
449
|
+
.dsm-pin[aria-pressed="true"] { color: var(--dsw-alias-label-primary); }
|
|
450
|
+
|
|
451
|
+
.dsm-notice { display: flex; gap: 8px; align-items: center; padding: 6px 6px 6px 12px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary);
|
|
452
|
+
background: var(--dsw-specific-menu, var(--dsw-alias-bg-overlay)); border-radius: var(--dsw-radius-lg); border: .5px solid var(--dsw-alias-border-l2);
|
|
453
|
+
box-shadow: var(--dsw-elevation-panel, 0 12px 32px #0006); animation: dsm-drop .45s cubic-bezier(.16, 1, .3, 1); }
|
|
454
|
+
.dsm-notice > svg { color: var(--dsw-alias-state-warn-primary); }
|
|
455
|
+
.dsm-notice.dsm-err > svg { color: var(--dsw-alias-state-error-primary); }
|
|
456
|
+
.dsm-notice-host { position: fixed; top: 52px; right: 20px; z-index: 1000; max-width: calc(100vw - 40px); }
|
|
457
|
+
@keyframes dsm-drop { from { opacity: 0; transform: translateY(-8px); filter: blur(4px); } }
|
|
458
|
+
|
|
459
|
+
.dsh-subscription-speed button:hover:not(:disabled) { --dsh-subscription-speed-hover: var(--dsw-alias-interactive-bg-hover); }
|
|
460
|
+
.dsh-subscription-speed button:active:not(:disabled) { --dsh-subscription-speed-hover: var(--dsw-alias-interactive-bg-active); }
|
|
461
|
+
.dsh-subscription-speed button:disabled { opacity: .4; cursor: default; }
|
|
462
|
+
.dsh-subscription-speed button:focus-visible { outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
|
|
463
|
+
|
|
464
|
+
@media (prefers-reduced-motion: reduce) { .dsm-sub *, .dsm-pop *, .dsm-notice, .dsm-fill { animation: none !important; transition: none !important; } }
|
|
465
|
+
@media (max-width: 640px) {
|
|
466
|
+
.dsm-wins { column-gap: 8px; grid-template-columns: fit-content(96px) minmax(40px, 1fr) 36px 84px; }
|
|
467
|
+
.dsm-minis { gap: 8px; } .dsm-mini { grid-template-columns: auto 22px 32px; } .dsm-mini i { max-width: 56px; }
|
|
468
|
+
.dsm-gp-name { display: none; }
|
|
469
|
+
.dsm-footer-block .dsm-row { flex-wrap: wrap; padding: 6px 0; }
|
|
470
|
+
}
|
|
471
|
+
`;
|
|
472
|
+
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/client/rpc.ts
|
|
475
|
+
/** Every provider this plugin can route, in Models-page order. Names and metering come from `status`. */
|
|
476
|
+
const PROVIDERS = [
|
|
477
|
+
"codex",
|
|
478
|
+
"claude",
|
|
479
|
+
"grok",
|
|
480
|
+
"copilot",
|
|
481
|
+
"antigravity"
|
|
482
|
+
];
|
|
483
|
+
/** Providers that read usage with a pasted key, in Usage-page order. Mirrors the Host's `KEY_PROVIDER_IDS`. */
|
|
484
|
+
const KEY_PROVIDERS = [
|
|
485
|
+
"zai",
|
|
486
|
+
"ollama-cloud",
|
|
487
|
+
"kimi"
|
|
488
|
+
];
|
|
489
|
+
const USAGE_PROVIDERS = [...PROVIDERS, ...KEY_PROVIDERS];
|
|
490
|
+
const isUsageProviderId = (value) => typeof value === "string" && USAGE_PROVIDERS.includes(value);
|
|
491
|
+
const isProviderId = (value) => typeof value === "string" && PROVIDERS.includes(value);
|
|
492
|
+
/** The provider's short name from `status`, else its id. */
|
|
493
|
+
const shortName = (status, provider) => status?.providers[provider]?.shortName ?? provider;
|
|
494
|
+
/** Whether the Host reports usage for this provider. */
|
|
495
|
+
const isMetered = (status, provider) => status?.providers[provider]?.metered === true;
|
|
496
|
+
/** The Host's business error text. */
|
|
497
|
+
var SubscriptionsAuthError = class extends Error {};
|
|
498
|
+
/**
|
|
499
|
+
* Call one endpoint and unwrap its business result.
|
|
500
|
+
* @throws SubscriptionsAuthError with the Host's message on transport or business failure.
|
|
501
|
+
*/
|
|
502
|
+
async function call(rpc, endpoint, payload) {
|
|
503
|
+
let result;
|
|
504
|
+
try {
|
|
505
|
+
result = await rpc.call("/api", `subscriptions-auth.${endpoint}`, payload);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
throw new SubscriptionsAuthError(error instanceof Error ? error.message : String(error));
|
|
508
|
+
}
|
|
509
|
+
if (!result.ok) throw new SubscriptionsAuthError(result.error.message);
|
|
510
|
+
return result.value;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/client/store.ts
|
|
515
|
+
const POLL_MS = 5 * 6e4;
|
|
516
|
+
const usageKey = (provider, account) => `${provider}/${account}`;
|
|
517
|
+
var SubscriptionsStore = class {
|
|
518
|
+
snapshot = {
|
|
519
|
+
status: void 0,
|
|
520
|
+
statusError: void 0,
|
|
521
|
+
usage: /* @__PURE__ */ new Map(),
|
|
522
|
+
refreshing: /* @__PURE__ */ new Set()
|
|
523
|
+
};
|
|
524
|
+
listeners = /* @__PURE__ */ new Set();
|
|
525
|
+
lastRevision = -1;
|
|
526
|
+
statusInFlight;
|
|
527
|
+
timer;
|
|
528
|
+
/** The session the gauge is showing, so `status` reports its serving account. */
|
|
529
|
+
sessionId;
|
|
530
|
+
constructor(rpc) {
|
|
531
|
+
this.rpc = rpc;
|
|
532
|
+
}
|
|
533
|
+
getSnapshot = () => this.snapshot;
|
|
534
|
+
subscribe = (listener) => {
|
|
535
|
+
this.listeners.add(listener);
|
|
536
|
+
if (this.listeners.size === 1) this.start();
|
|
537
|
+
return () => {
|
|
538
|
+
this.listeners.delete(listener);
|
|
539
|
+
if (this.listeners.size === 0) this.stop();
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
/** Report the session whose serving account the `status` read should name. */
|
|
543
|
+
focusSession(sessionId) {
|
|
544
|
+
if (this.sessionId === sessionId) return;
|
|
545
|
+
this.sessionId = sessionId;
|
|
546
|
+
this.refreshStatus();
|
|
547
|
+
}
|
|
548
|
+
/** Re-read login status; usage follows when the Host's revision moved. */
|
|
549
|
+
refreshStatus() {
|
|
550
|
+
this.statusInFlight ??= this.readStatus().finally(() => {
|
|
551
|
+
this.statusInFlight = void 0;
|
|
552
|
+
});
|
|
553
|
+
return this.statusInFlight;
|
|
554
|
+
}
|
|
555
|
+
/** Read every signed-in account's usage under one reason. */
|
|
556
|
+
async refreshUsage(reason, only) {
|
|
557
|
+
const status = this.snapshot.status;
|
|
558
|
+
if (status === void 0) return;
|
|
559
|
+
await Promise.all(USAGE_PROVIDERS.filter((p) => (only === void 0 || p === only) && isMetered(status, p)).flatMap((provider) => (status.providers[provider]?.accounts ?? []).map((account) => this.readUsage(provider, account.key, reason))));
|
|
560
|
+
}
|
|
561
|
+
async readStatus() {
|
|
562
|
+
try {
|
|
563
|
+
const status = await call(this.rpc, "status", this.sessionId === void 0 ? {} : { sessionId: this.sessionId });
|
|
564
|
+
const known = new Set(USAGE_PROVIDERS.flatMap((p) => (status.providers[p]?.accounts ?? []).map((a) => usageKey(p, a.key))));
|
|
565
|
+
const usage = new Map([...this.snapshot.usage].filter(([key]) => known.has(key)));
|
|
566
|
+
this.publish({
|
|
567
|
+
...this.snapshot,
|
|
568
|
+
status,
|
|
569
|
+
statusError: void 0,
|
|
570
|
+
usage
|
|
571
|
+
});
|
|
572
|
+
if (status.usageRevision !== this.lastRevision) {
|
|
573
|
+
this.lastRevision = status.usageRevision;
|
|
574
|
+
await this.refreshUsage("poll");
|
|
575
|
+
}
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.publish({
|
|
578
|
+
...this.snapshot,
|
|
579
|
+
statusError: error instanceof Error ? error.message : String(error)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async readUsage(provider, account, reason) {
|
|
584
|
+
const key = usageKey(provider, account);
|
|
585
|
+
if (!this.snapshot.usage.has(key)) this.setUsage(key, { kind: "loading" });
|
|
586
|
+
this.publish({
|
|
587
|
+
...this.snapshot,
|
|
588
|
+
refreshing: new Set([...this.snapshot.refreshing, key])
|
|
589
|
+
});
|
|
590
|
+
try {
|
|
591
|
+
const usage = await call(this.rpc, "usage", {
|
|
592
|
+
provider,
|
|
593
|
+
account,
|
|
594
|
+
reason
|
|
595
|
+
});
|
|
596
|
+
this.setUsage(key, {
|
|
597
|
+
kind: "ready",
|
|
598
|
+
usage,
|
|
599
|
+
receivedAt: Date.now()
|
|
600
|
+
});
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (this.snapshot.usage.get(key)?.kind !== "ready") this.setUsage(key, {
|
|
603
|
+
kind: "error",
|
|
604
|
+
message: error instanceof Error ? error.message : String(error)
|
|
605
|
+
});
|
|
606
|
+
} finally {
|
|
607
|
+
const refreshing = new Set(this.snapshot.refreshing);
|
|
608
|
+
refreshing.delete(key);
|
|
609
|
+
this.publish({
|
|
610
|
+
...this.snapshot,
|
|
611
|
+
refreshing
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
setUsage(key, entry) {
|
|
616
|
+
const usage = new Map(this.snapshot.usage);
|
|
617
|
+
usage.set(key, entry);
|
|
618
|
+
this.publish({
|
|
619
|
+
...this.snapshot,
|
|
620
|
+
usage
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
publish(next) {
|
|
624
|
+
this.snapshot = next;
|
|
625
|
+
for (const listener of this.listeners) listener();
|
|
626
|
+
}
|
|
627
|
+
onVisibility = () => {
|
|
628
|
+
if (document.visibilityState === "visible") this.refreshStatus();
|
|
629
|
+
};
|
|
630
|
+
start() {
|
|
631
|
+
this.refreshStatus();
|
|
632
|
+
this.timer = setInterval(() => {
|
|
633
|
+
if (document.visibilityState !== "visible") return;
|
|
634
|
+
this.refreshStatus().then(() => this.refreshUsage("poll"));
|
|
635
|
+
}, POLL_MS);
|
|
636
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
637
|
+
}
|
|
638
|
+
stop() {
|
|
639
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
640
|
+
this.timer = void 0;
|
|
641
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
/** The account a provider is using now: the one that served last, else the default, else the first. */
|
|
645
|
+
function activeAccountKey(status) {
|
|
646
|
+
if (status === void 0 || status.accounts.length === 0) return void 0;
|
|
647
|
+
const served = status.servingAccount;
|
|
648
|
+
if (served !== void 0 && status.accounts.some((a) => a.key === served)) return served;
|
|
649
|
+
return (status.accounts.find((a) => a.isDefault) ?? status.accounts[0])?.key;
|
|
650
|
+
}
|
|
651
|
+
/** The active account shown in full, and the others that fold behind one row. */
|
|
652
|
+
function splitAccounts(status) {
|
|
653
|
+
const key = activeAccountKey(status);
|
|
654
|
+
const active = status.accounts.find((a) => a.key === key);
|
|
655
|
+
if (active === void 0) return void 0;
|
|
656
|
+
return {
|
|
657
|
+
active,
|
|
658
|
+
rest: status.accounts.filter((a) => a !== active)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/client/prefs.ts
|
|
664
|
+
const DEFAULT_PREFS = {
|
|
665
|
+
composer: true,
|
|
666
|
+
pinned: [],
|
|
667
|
+
display: "left",
|
|
668
|
+
paceWarnings: true,
|
|
669
|
+
writable: false
|
|
670
|
+
};
|
|
671
|
+
/** Narrow the namespace section's `usage` field, falling back field by field. */
|
|
672
|
+
function parsePrefs(section, writable) {
|
|
673
|
+
const usage = section !== null && typeof section === "object" && "usage" in section ? section.usage : void 0;
|
|
674
|
+
if (usage === null || typeof usage !== "object") return {
|
|
675
|
+
...DEFAULT_PREFS,
|
|
676
|
+
writable
|
|
677
|
+
};
|
|
678
|
+
const read = (key) => key in usage ? usage[key] : void 0;
|
|
679
|
+
const composer = read("composer");
|
|
680
|
+
const legacyGauge = read("gauge");
|
|
681
|
+
const display = read("display");
|
|
682
|
+
const pinned = read("pinned");
|
|
683
|
+
const pace$1 = read("paceWarnings");
|
|
684
|
+
return {
|
|
685
|
+
composer: typeof composer === "boolean" ? composer : legacyGauge !== "off",
|
|
686
|
+
pinned: Array.isArray(pinned) ? pinned.filter(isUsageProviderId) : [],
|
|
687
|
+
display: display === "used" ? "used" : "left",
|
|
688
|
+
paceWarnings: typeof pace$1 === "boolean" ? pace$1 : true,
|
|
689
|
+
writable
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
var PrefsStore = class {
|
|
693
|
+
snapshot = DEFAULT_PREFS;
|
|
694
|
+
listeners = /* @__PURE__ */ new Set();
|
|
695
|
+
unsubscribe;
|
|
696
|
+
form;
|
|
697
|
+
getSnapshot = () => this.snapshot;
|
|
698
|
+
subscribe = (listener) => {
|
|
699
|
+
this.listeners.add(listener);
|
|
700
|
+
return () => {
|
|
701
|
+
this.listeners.delete(listener);
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
/** Bind the form once the settings service is available; returns the unbinder. */
|
|
705
|
+
bind(form) {
|
|
706
|
+
this.form = form;
|
|
707
|
+
const sync = () => {
|
|
708
|
+
const view = form.getSnapshot();
|
|
709
|
+
if (view.status !== "ready") return;
|
|
710
|
+
this.publish(parsePrefs(view.value, view.writable));
|
|
711
|
+
};
|
|
712
|
+
this.unsubscribe = form.subscribe(sync);
|
|
713
|
+
sync();
|
|
714
|
+
return () => {
|
|
715
|
+
this.unsubscribe?.();
|
|
716
|
+
this.form = void 0;
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/** Change some preferences; the shown value updates at once and settles on the Host's answer. */
|
|
720
|
+
async set(patch) {
|
|
721
|
+
const next = {
|
|
722
|
+
...this.snapshot,
|
|
723
|
+
...patch
|
|
724
|
+
};
|
|
725
|
+
this.publish(next);
|
|
726
|
+
if (this.form === void 0) return false;
|
|
727
|
+
const ops = Object.entries(patch).map(([key, value]) => ({
|
|
728
|
+
op: "set",
|
|
729
|
+
path: ["usage", key],
|
|
730
|
+
value
|
|
731
|
+
}));
|
|
732
|
+
ops.push({
|
|
733
|
+
op: "unset",
|
|
734
|
+
path: ["usage", "gauge"]
|
|
735
|
+
});
|
|
736
|
+
return this.form.mutate(ops);
|
|
737
|
+
}
|
|
738
|
+
togglePinned(provider) {
|
|
739
|
+
const pinned = this.snapshot.pinned.includes(provider) ? this.snapshot.pinned.filter((p) => p !== provider) : [...this.snapshot.pinned, provider];
|
|
740
|
+
return this.set({ pinned });
|
|
741
|
+
}
|
|
742
|
+
publish(next) {
|
|
743
|
+
this.snapshot = next;
|
|
744
|
+
for (const listener of this.listeners) listener();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/usage/pace.ts
|
|
750
|
+
/** Below this share of elapsed window a burst says little about the whole window. */
|
|
751
|
+
const MIN_FORECAST_ELAPSED = .2;
|
|
752
|
+
/** Below this usage the projection is noise. */
|
|
753
|
+
const MIN_FORECAST_USED_PERCENT = 3;
|
|
754
|
+
/** Projected use above this percent turns a window `tight`. */
|
|
755
|
+
const TIGHT_PROJECTED_PERCENT = 85;
|
|
756
|
+
/**
|
|
757
|
+
* Compute the pace of one window at `now`.
|
|
758
|
+
* @param window - the provider-reported window.
|
|
759
|
+
* @param now - epoch milliseconds.
|
|
760
|
+
* @returns the pace; `elapsed` is the share of the window passed, 0 to 1.
|
|
761
|
+
*/
|
|
762
|
+
function pace(window$1, now) {
|
|
763
|
+
const used = window$1.usedPercent;
|
|
764
|
+
if (!Number.isFinite(used)) return { state: "unknown" };
|
|
765
|
+
const usedPercent = Math.max(0, Math.min(100, used));
|
|
766
|
+
if (usedPercent >= 100) return {
|
|
767
|
+
state: "out",
|
|
768
|
+
usedPercent
|
|
769
|
+
};
|
|
770
|
+
const { durationMs, resetsAt } = window$1;
|
|
771
|
+
if (durationMs === void 0 || resetsAt === void 0 || !(durationMs > 0) || !Number.isFinite(resetsAt)) return {
|
|
772
|
+
state: "calm",
|
|
773
|
+
usedPercent
|
|
774
|
+
};
|
|
775
|
+
const elapsed = Math.min(1, Math.max(0, 1 - (resetsAt - now) / durationMs));
|
|
776
|
+
if (elapsed < MIN_FORECAST_ELAPSED || usedPercent < MIN_FORECAST_USED_PERCENT) return {
|
|
777
|
+
state: "calm",
|
|
778
|
+
usedPercent,
|
|
779
|
+
elapsed
|
|
780
|
+
};
|
|
781
|
+
const projectedPercent = usedPercent / elapsed;
|
|
782
|
+
if (projectedPercent > 100) {
|
|
783
|
+
const ratePerMs = usedPercent / (elapsed * durationMs);
|
|
784
|
+
return {
|
|
785
|
+
state: "over",
|
|
786
|
+
usedPercent,
|
|
787
|
+
elapsed,
|
|
788
|
+
projectedPercent,
|
|
789
|
+
emptyAt: now + (100 - usedPercent) / ratePerMs
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (projectedPercent > TIGHT_PROJECTED_PERCENT) return {
|
|
793
|
+
state: "tight",
|
|
794
|
+
usedPercent,
|
|
795
|
+
elapsed,
|
|
796
|
+
projectedPercent
|
|
797
|
+
};
|
|
798
|
+
return {
|
|
799
|
+
state: "calm",
|
|
800
|
+
usedPercent,
|
|
801
|
+
elapsed,
|
|
802
|
+
projectedPercent
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* The percent a meter shows.
|
|
807
|
+
* @param window - the window to show.
|
|
808
|
+
* @param display - left or used.
|
|
809
|
+
* @returns an integer percent, or undefined when the provider reported none.
|
|
810
|
+
*/
|
|
811
|
+
function displayPercent(window$1, display) {
|
|
812
|
+
if (!Number.isFinite(window$1.usedPercent)) return void 0;
|
|
813
|
+
const used = Math.round(Math.max(0, Math.min(100, window$1.usedPercent)));
|
|
814
|
+
return display === "left" ? 100 - used : used;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/client/meter.tsx
|
|
819
|
+
/** Translate with placeholders. */
|
|
820
|
+
function tf(t, key, params) {
|
|
821
|
+
return format(t(key), params);
|
|
822
|
+
}
|
|
823
|
+
const PATHS = {
|
|
824
|
+
refresh: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.5 8a5.5 5.5 0 1 1-1.6-3.9" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.5 2.5v3h-3" })] }),
|
|
825
|
+
alert: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.8 15 14H1z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.3v3.4M8 11.8v.01" })] }),
|
|
826
|
+
check: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m3.5 8.3 3 3 6-6.6" }),
|
|
827
|
+
star: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
828
|
+
d: "m8 2 1.8 3.8 4.2.5-3.1 2.9.8 4.1L8 11.3 4.3 13.3l.8-4.1L2 6.3l4.2-.5z",
|
|
829
|
+
fill: "currentColor"
|
|
830
|
+
}),
|
|
831
|
+
bolt: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
832
|
+
d: "M9 1.8 3.5 9H8l-1 5.2L12.5 7H8z",
|
|
833
|
+
fill: "currentColor",
|
|
834
|
+
stroke: "none"
|
|
835
|
+
}),
|
|
836
|
+
pin: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 2.5h4l-.6 4 2.1 2.2v1H4.5v-1l2.1-2.2z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 9.7v3.8" })] }),
|
|
837
|
+
pinFilled: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
838
|
+
d: "M6 2.5h4l-.6 4 2.1 2.2v1H4.5v-1l2.1-2.2z",
|
|
839
|
+
fill: "currentColor"
|
|
840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 9.7v3.8" })] }),
|
|
841
|
+
gear: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
842
|
+
cx: "8",
|
|
843
|
+
cy: "8",
|
|
844
|
+
r: "2.2"
|
|
845
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.8v1.6M8 12.6v1.6M1.8 8h1.6M12.6 8h1.6M3.6 3.6l1.1 1.1M11.3 11.3l1.1 1.1M3.6 12.4l1.1-1.1M11.3 4.7l1.1-1.1" })] }),
|
|
846
|
+
ext: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9 3h4v4M13 3 7.5 8.5M12 9.5V13H3V4h3.5" }),
|
|
847
|
+
copy: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
848
|
+
x: "5",
|
|
849
|
+
y: "5",
|
|
850
|
+
width: "8.5",
|
|
851
|
+
height: "8.5",
|
|
852
|
+
rx: "1.5"
|
|
853
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" })] }),
|
|
854
|
+
x: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m4 4 8 8M12 4l-8 8" }),
|
|
855
|
+
right: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 4 4 4-4 4" }),
|
|
856
|
+
down: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m5 6.5 3 3 3-3" }),
|
|
857
|
+
meter: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 11a5.5 5.5 0 0 1 11 0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m8 11 2.6-3.2" })] }),
|
|
858
|
+
sparkle: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
859
|
+
d: "M8 1.5 9.4 6.6 14.5 8 9.4 9.4 8 14.5 6.6 9.4 1.5 8 6.6 6.6z",
|
|
860
|
+
fill: "currentColor",
|
|
861
|
+
stroke: "none"
|
|
862
|
+
})
|
|
863
|
+
};
|
|
864
|
+
/** One 16-unit stroke icon. */
|
|
865
|
+
function Icon({ name, size = 14, className }) {
|
|
866
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
867
|
+
className,
|
|
868
|
+
width: size,
|
|
869
|
+
height: size,
|
|
870
|
+
viewBox: "0 0 16 16",
|
|
871
|
+
fill: "none",
|
|
872
|
+
stroke: "currentColor",
|
|
873
|
+
strokeWidth: 1.5,
|
|
874
|
+
strokeLinecap: "round",
|
|
875
|
+
strokeLinejoin: "round",
|
|
876
|
+
"aria-hidden": "true",
|
|
877
|
+
style: {
|
|
878
|
+
flex: "none",
|
|
879
|
+
display: "block"
|
|
880
|
+
},
|
|
881
|
+
children: PATHS[name]
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
const D = 24 * 36e5;
|
|
885
|
+
/** "3h 6m", "2d 12h", "42m". */
|
|
886
|
+
function shortDuration(ms) {
|
|
887
|
+
const total = Math.max(1, Math.round(ms / 6e4));
|
|
888
|
+
const days = Math.floor(total / 1440);
|
|
889
|
+
const hours = Math.floor(total % 1440 / 60);
|
|
890
|
+
const minutes = total % 60;
|
|
891
|
+
if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
892
|
+
if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
893
|
+
return `${Math.max(1, minutes)}m`;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* A short point in time in the browser's locale: "6:04 PM" today, "Wed" within
|
|
897
|
+
* the week, "12 Nov" beyond. The day alone keeps the time column narrow; the
|
|
898
|
+
* tooltip carries the exact reset time.
|
|
899
|
+
*/
|
|
900
|
+
function clockTime(at, now) {
|
|
901
|
+
const date = new Date(at);
|
|
902
|
+
if (date.toDateString() === new Date(now).toDateString()) return date.toLocaleTimeString(void 0, {
|
|
903
|
+
hour: "numeric",
|
|
904
|
+
minute: "2-digit"
|
|
905
|
+
});
|
|
906
|
+
if (Math.abs(at - now) < 6 * D) return date.toLocaleDateString(void 0, { weekday: "short" });
|
|
907
|
+
return date.toLocaleDateString(void 0, {
|
|
908
|
+
day: "numeric",
|
|
909
|
+
month: "short"
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/** Weekday and time, for tooltips. */
|
|
913
|
+
function fullTime(at) {
|
|
914
|
+
return new Date(at).toLocaleString(void 0, {
|
|
915
|
+
weekday: "short",
|
|
916
|
+
hour: "numeric",
|
|
917
|
+
minute: "2-digit"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/** The window's length as "5h", "7d" or "30d", rounded to a whole unit; undefined when unknown. */
|
|
921
|
+
function lengthLabel(durationMs) {
|
|
922
|
+
if (durationMs === void 0 || !Number.isFinite(durationMs) || durationMs <= 0) return void 0;
|
|
923
|
+
const hours = Math.round(durationMs / 36e5);
|
|
924
|
+
if (hours < 1) return `${Math.max(1, Math.round(durationMs / 6e4))}m`;
|
|
925
|
+
return hours < 48 ? `${hours}h` : `${Math.round(hours / 24)}d`;
|
|
926
|
+
}
|
|
927
|
+
/** "5h", "7d", "30d": the window's own length, else its kind, else "limit". */
|
|
928
|
+
function kindLabel(t, kind, durationMs) {
|
|
929
|
+
const length = lengthLabel(durationMs);
|
|
930
|
+
if (length !== void 0) return length;
|
|
931
|
+
return kind === "session" ? t("window5h") : kind === "weekly" ? t("window7d") : t("windowOther");
|
|
932
|
+
}
|
|
933
|
+
/** The length label, with a model scope when the window has one. A per-model quota is named by its scope alone. */
|
|
934
|
+
function windowLabel(t, window$1) {
|
|
935
|
+
if (window$1.scope === void 0) return kindLabel(t, window$1.kind, window$1.durationMs);
|
|
936
|
+
return window$1.kind === "other" ? window$1.scope : `${kindLabel(t, window$1.kind, window$1.durationMs)} ${window$1.scope}`;
|
|
937
|
+
}
|
|
938
|
+
const FAMILY_NAMES = {
|
|
939
|
+
gemini: "Gemini",
|
|
940
|
+
claude: "Claude",
|
|
941
|
+
gpt: "GPT"
|
|
942
|
+
};
|
|
943
|
+
/** "claude-opus-4-6-thinking" becomes "Claude"; "tab_flash_lite" becomes "Tab". */
|
|
944
|
+
function modelFamily(id) {
|
|
945
|
+
const head = id.split(/[-_]/)[0] ?? id;
|
|
946
|
+
return FAMILY_NAMES[head.toLowerCase()] ?? head.charAt(0).toUpperCase() + head.slice(1);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Per-model windows that report the same usage and reset share one quota pool
|
|
950
|
+
* (Antigravity reports one window per model), so they merge into one row named
|
|
951
|
+
* by model family. A per-model window at 0% with no reset carries no information
|
|
952
|
+
* and is hidden. Unscoped windows pass through unchanged.
|
|
953
|
+
*/
|
|
954
|
+
function shownWindows(windows) {
|
|
955
|
+
const out = [];
|
|
956
|
+
const groups = /* @__PURE__ */ new Map();
|
|
957
|
+
for (const window$1 of windows ?? []) {
|
|
958
|
+
if (window$1.scope === void 0 || window$1.kind !== "other") {
|
|
959
|
+
out.push(window$1);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (window$1.resetsAt === void 0 && window$1.usedPercent === 0) continue;
|
|
963
|
+
const minute = window$1.resetsAt === void 0 ? "" : String(Math.round(window$1.resetsAt / 6e4));
|
|
964
|
+
const key = `${Math.round(window$1.usedPercent)}/${minute}`;
|
|
965
|
+
const group = groups.get(key);
|
|
966
|
+
if (group === void 0) {
|
|
967
|
+
const entry = {
|
|
968
|
+
window: window$1,
|
|
969
|
+
models: [window$1.scope]
|
|
970
|
+
};
|
|
971
|
+
groups.set(key, entry);
|
|
972
|
+
} else group.models.push(window$1.scope);
|
|
973
|
+
}
|
|
974
|
+
for (const { window: window$1, models } of groups.values()) {
|
|
975
|
+
const families = [...new Set(models.map(modelFamily))];
|
|
976
|
+
out.push({
|
|
977
|
+
...window$1,
|
|
978
|
+
scope: families.join(" · "),
|
|
979
|
+
models
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* What compact surfaces show: unscoped windows, session first. A provider that
|
|
986
|
+
* only reports per-model windows (Antigravity) shows its two most used instead.
|
|
987
|
+
*/
|
|
988
|
+
function primaryWindows(windows) {
|
|
989
|
+
const order = {
|
|
990
|
+
session: 0,
|
|
991
|
+
weekly: 1,
|
|
992
|
+
other: 2
|
|
993
|
+
};
|
|
994
|
+
const all = shownWindows(windows);
|
|
995
|
+
const unscoped = all.filter((w) => w.scope === void 0).sort((a, b) => order[a.kind] - order[b.kind]);
|
|
996
|
+
if (unscoped.length > 0) return unscoped;
|
|
997
|
+
return [...all].sort((a, b) => b.usedPercent - a.usedPercent).slice(0, 2);
|
|
998
|
+
}
|
|
999
|
+
/** The pace bar: fill is quota, hatch is the forecast, color is the pace state. Drains in `left` mode. */
|
|
1000
|
+
function Bar({ window: window$1, p, display, height = 6 }) {
|
|
1001
|
+
const used = p.state === "unknown" ? 0 : p.usedPercent;
|
|
1002
|
+
const left = display === "left";
|
|
1003
|
+
const fill = left ? 100 - used : used;
|
|
1004
|
+
let projection = null;
|
|
1005
|
+
if (p.state === "over" || p.state === "tight") {
|
|
1006
|
+
const end = Math.min(100, p.projectedPercent);
|
|
1007
|
+
projection = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1008
|
+
className: "dsm-proj",
|
|
1009
|
+
style: left ? {
|
|
1010
|
+
left: `${100 - end}%`,
|
|
1011
|
+
width: `${end - used}%`
|
|
1012
|
+
} : {
|
|
1013
|
+
left: `${used}%`,
|
|
1014
|
+
width: `${end - used}%`
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1019
|
+
className: `dsm-bar dsm-${p.state}`,
|
|
1020
|
+
style: { height },
|
|
1021
|
+
role: "meter",
|
|
1022
|
+
"aria-valuemin": 0,
|
|
1023
|
+
"aria-valuemax": 100,
|
|
1024
|
+
"aria-valuenow": displayPercent(window$1, display) ?? 0,
|
|
1025
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1026
|
+
className: "dsm-fill",
|
|
1027
|
+
style: { width: `${fill}%` }
|
|
1028
|
+
}), projection]
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function valueText(window$1, display) {
|
|
1032
|
+
const value = displayPercent(window$1, display);
|
|
1033
|
+
return value === void 0 ? "–" : `${value}%`;
|
|
1034
|
+
}
|
|
1035
|
+
/** The time column: when this changes next. */
|
|
1036
|
+
function timeText(t, window$1, p, now) {
|
|
1037
|
+
if (p.state === "out" && window$1.resetsAt !== void 0) return tf(t, "backAt", { time: clockTime(window$1.resetsAt, now) });
|
|
1038
|
+
if (p.state === "over") return tf(t, "emptyAt", { time: clockTime(p.emptyAt, now) });
|
|
1039
|
+
return window$1.resetsAt === void 0 ? "" : tf(t, "resetsIn", { time: shortDuration(Math.max(0, window$1.resetsAt - now)) });
|
|
1040
|
+
}
|
|
1041
|
+
function tooltip(t, window$1, p, now) {
|
|
1042
|
+
if (p.state === "unknown") return t("notReported");
|
|
1043
|
+
const used = Math.round(p.usedPercent);
|
|
1044
|
+
const lines = [...window$1.models ?? [], tf(t, "tipUsedLeft", {
|
|
1045
|
+
used,
|
|
1046
|
+
left: 100 - used
|
|
1047
|
+
})];
|
|
1048
|
+
if (window$1.used !== void 0 && window$1.limit !== void 0) lines.push(tf(t, "tipCount", {
|
|
1049
|
+
used: Math.round(window$1.used),
|
|
1050
|
+
limit: window$1.limit
|
|
1051
|
+
}));
|
|
1052
|
+
if ((p.state === "calm" || p.state === "tight" || p.state === "over") && p.elapsed !== void 0) lines.push(tf(t, "tipElapsed", { percent: Math.round(p.elapsed * 100) }));
|
|
1053
|
+
if (p.state === "tight") lines.push(tf(t, "tipPace", { percent: Math.round(p.projectedPercent) }));
|
|
1054
|
+
if (p.state === "over") lines.push(tf(t, "emptyAt", { time: fullTime(p.emptyAt) }));
|
|
1055
|
+
if (window$1.resetsAt !== void 0) lines.push(tf(t, "tipResets", { time: fullTime(window$1.resetsAt) }));
|
|
1056
|
+
return lines.join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
/** One window on the shared grid: label | bar | value | time. */
|
|
1059
|
+
function WindowRow({ t, window: window$1, display, now }) {
|
|
1060
|
+
const p = pace(window$1, now);
|
|
1061
|
+
const tip = tooltip(t, window$1, p, now);
|
|
1062
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1063
|
+
className: `dsm-w dsm-${p.state}`,
|
|
1064
|
+
children: [
|
|
1065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1066
|
+
className: "dsm-wl",
|
|
1067
|
+
title: window$1.scope === void 0 ? tip : `${windowLabel(t, window$1)}\n${tip}`,
|
|
1068
|
+
children: windowLabel(t, window$1)
|
|
1069
|
+
}),
|
|
1070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1071
|
+
title: tip,
|
|
1072
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bar, {
|
|
1073
|
+
window: window$1,
|
|
1074
|
+
p,
|
|
1075
|
+
display
|
|
1076
|
+
})
|
|
1077
|
+
}),
|
|
1078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1079
|
+
className: "dsm-wv",
|
|
1080
|
+
title: tip,
|
|
1081
|
+
children: valueText(window$1, display)
|
|
1082
|
+
}),
|
|
1083
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1084
|
+
className: "dsm-wt",
|
|
1085
|
+
title: tip,
|
|
1086
|
+
children: timeText(t, window$1, p, now)
|
|
1087
|
+
})
|
|
1088
|
+
]
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
/** Label, small bar and value, for summary rows and the gauge. */
|
|
1092
|
+
function Mini({ t, window: window$1, display, now }) {
|
|
1093
|
+
const p = pace(window$1, now);
|
|
1094
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1095
|
+
className: `dsm-mini dsm-${p.state}`,
|
|
1096
|
+
title: window$1.scope === void 0 ? tooltip(t, window$1, p, now) : `${window$1.scope}\n${tooltip(t, window$1, p, now)}`,
|
|
1097
|
+
children: [
|
|
1098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { children: window$1.kind === "other" && window$1.scope !== void 0 ? window$1.scope : kindLabel(t, window$1.kind, window$1.durationMs) }),
|
|
1099
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bar, {
|
|
1100
|
+
window: window$1,
|
|
1101
|
+
p,
|
|
1102
|
+
display,
|
|
1103
|
+
height: 4
|
|
1104
|
+
}),
|
|
1105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: valueText(window$1, display) })
|
|
1106
|
+
]
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/** The mini meters of an account's unscoped windows. */
|
|
1110
|
+
function Minis({ t, windows, display, now }) {
|
|
1111
|
+
const shown = primaryWindows(windows);
|
|
1112
|
+
if (shown.length === 0) return null;
|
|
1113
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1114
|
+
className: "dsm-minis",
|
|
1115
|
+
children: shown.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Mini, {
|
|
1116
|
+
t,
|
|
1117
|
+
window: w,
|
|
1118
|
+
display,
|
|
1119
|
+
now
|
|
1120
|
+
}, i))
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/client/hooks.ts
|
|
1126
|
+
/** The current time, re-read every `intervalMs`, so countdowns advance without a fetch. */
|
|
1127
|
+
function useNow(intervalMs) {
|
|
1128
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
1129
|
+
(0, react.useEffect)(() => {
|
|
1130
|
+
const timer = setInterval(() => {
|
|
1131
|
+
setNow(Date.now());
|
|
1132
|
+
}, intervalMs);
|
|
1133
|
+
return () => {
|
|
1134
|
+
clearInterval(timer);
|
|
1135
|
+
};
|
|
1136
|
+
}, [intervalMs]);
|
|
1137
|
+
return now;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/client/context.ts
|
|
1142
|
+
function useStore(store) {
|
|
1143
|
+
return (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
1144
|
+
}
|
|
1145
|
+
function usePrefs(prefs) {
|
|
1146
|
+
return (0, react.useSyncExternalStore)(prefs.subscribe, prefs.getSnapshot);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/client/ProviderCard.tsx
|
|
1151
|
+
/** How often a busy sign-in re-reads status until the account lands. */
|
|
1152
|
+
const SIGN_IN_POLL_MS = 2e3;
|
|
1153
|
+
function ProviderCard(props) {
|
|
1154
|
+
const { store, prefs, rpc, t } = props;
|
|
1155
|
+
const provider = props.provider.provider;
|
|
1156
|
+
if (store === void 0 || prefs === void 0 || rpc === void 0 || t === void 0 || !isProviderId(provider)) return null;
|
|
1157
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, {
|
|
1158
|
+
provider,
|
|
1159
|
+
store,
|
|
1160
|
+
prefs,
|
|
1161
|
+
rpc,
|
|
1162
|
+
t
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
function CardBody({ provider, store, prefs, rpc, t }) {
|
|
1166
|
+
const snapshot = useStore(store);
|
|
1167
|
+
const prefView = usePrefs(prefs);
|
|
1168
|
+
const now = useNow(3e4);
|
|
1169
|
+
const status = snapshot.status?.providers[provider];
|
|
1170
|
+
const [mode, setMode] = (0, react.useState)({ kind: "view" });
|
|
1171
|
+
const [error, setError] = (0, react.useState)();
|
|
1172
|
+
const [moreOpen, setMoreOpen] = (0, react.useState)(false);
|
|
1173
|
+
const busy = status?.busy === true;
|
|
1174
|
+
(0, react.useEffect)(() => {
|
|
1175
|
+
if (mode.kind !== "signin" && !busy) return;
|
|
1176
|
+
const timer = setInterval(() => {
|
|
1177
|
+
store.refreshStatus();
|
|
1178
|
+
}, SIGN_IN_POLL_MS);
|
|
1179
|
+
return () => {
|
|
1180
|
+
clearInterval(timer);
|
|
1181
|
+
};
|
|
1182
|
+
}, [
|
|
1183
|
+
mode.kind,
|
|
1184
|
+
busy,
|
|
1185
|
+
store
|
|
1186
|
+
]);
|
|
1187
|
+
const accountCount = status?.accounts.length ?? 0;
|
|
1188
|
+
const startCount = (0, react.useRef)(accountCount);
|
|
1189
|
+
(0, react.useEffect)(() => {
|
|
1190
|
+
if (mode.kind === "signin" && !busy && accountCount !== startCount.current) setMode({ kind: "view" });
|
|
1191
|
+
}, [
|
|
1192
|
+
mode.kind,
|
|
1193
|
+
busy,
|
|
1194
|
+
accountCount
|
|
1195
|
+
]);
|
|
1196
|
+
(0, react.useEffect)(() => {
|
|
1197
|
+
if (status?.detail !== void 0) setError(status.detail);
|
|
1198
|
+
}, [status?.detail]);
|
|
1199
|
+
const signIn = async (method) => {
|
|
1200
|
+
setError(void 0);
|
|
1201
|
+
startCount.current = accountCount;
|
|
1202
|
+
try {
|
|
1203
|
+
const response = await call(rpc, "login", {
|
|
1204
|
+
provider,
|
|
1205
|
+
...method === void 0 ? {} : { method }
|
|
1206
|
+
});
|
|
1207
|
+
if (response.authorizeUrl === "") {
|
|
1208
|
+
await store.refreshStatus();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (response.userCode !== void 0 && response.userCode !== "") setMode({
|
|
1212
|
+
kind: "signin",
|
|
1213
|
+
userCode: response.userCode,
|
|
1214
|
+
url: response.authorizeUrl
|
|
1215
|
+
});
|
|
1216
|
+
else {
|
|
1217
|
+
window.open(response.authorizeUrl, "_blank", "noopener");
|
|
1218
|
+
setMode({ kind: "signin" });
|
|
1219
|
+
}
|
|
1220
|
+
store.refreshStatus();
|
|
1221
|
+
} catch (failure) {
|
|
1222
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
const cancel = async () => {
|
|
1226
|
+
setMode({ kind: "view" });
|
|
1227
|
+
await call(rpc, "cancel", { provider }).catch(() => void 0);
|
|
1228
|
+
await store.refreshStatus();
|
|
1229
|
+
};
|
|
1230
|
+
if (status === void 0) return snapshot.statusError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1231
|
+
className: "dsm-sub",
|
|
1232
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1233
|
+
className: "dsm-card-error",
|
|
1234
|
+
children: snapshot.statusError
|
|
1235
|
+
})
|
|
1236
|
+
});
|
|
1237
|
+
if (mode.kind === "signin") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1238
|
+
className: "dsm-sub",
|
|
1239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SignIn, {
|
|
1240
|
+
t,
|
|
1241
|
+
provider,
|
|
1242
|
+
mode,
|
|
1243
|
+
rpc,
|
|
1244
|
+
onCancel: () => {
|
|
1245
|
+
cancel();
|
|
1246
|
+
},
|
|
1247
|
+
onError: setError
|
|
1248
|
+
}), error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1249
|
+
className: "dsm-card-error",
|
|
1250
|
+
role: "alert",
|
|
1251
|
+
children: error
|
|
1252
|
+
})]
|
|
1253
|
+
});
|
|
1254
|
+
if (status.accounts.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1255
|
+
className: "dsm-sub",
|
|
1256
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1257
|
+
className: "dsm-foot",
|
|
1258
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1259
|
+
type: "button",
|
|
1260
|
+
className: "dsm-btn dsm-primary",
|
|
1261
|
+
onClick: () => {
|
|
1262
|
+
signIn();
|
|
1263
|
+
},
|
|
1264
|
+
children: t("signIn")
|
|
1265
|
+
}), provider === "claude" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1266
|
+
type: "button",
|
|
1267
|
+
className: "dsm-btn dsm-ghost",
|
|
1268
|
+
onClick: () => {
|
|
1269
|
+
signIn("keychain");
|
|
1270
|
+
},
|
|
1271
|
+
children: t("claudeImport")
|
|
1272
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1273
|
+
className: "dsm-muted",
|
|
1274
|
+
children: t("noApiKey")
|
|
1275
|
+
})]
|
|
1276
|
+
}), error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1277
|
+
className: "dsm-card-error",
|
|
1278
|
+
role: "alert",
|
|
1279
|
+
children: error
|
|
1280
|
+
})]
|
|
1281
|
+
});
|
|
1282
|
+
const split = splitAccounts(status);
|
|
1283
|
+
if (split === void 0) return null;
|
|
1284
|
+
const { active, rest } = split;
|
|
1285
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1286
|
+
className: "dsm-sub",
|
|
1287
|
+
children: [
|
|
1288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
1289
|
+
t,
|
|
1290
|
+
provider,
|
|
1291
|
+
account: active,
|
|
1292
|
+
status,
|
|
1293
|
+
snapshot,
|
|
1294
|
+
prefs: prefView,
|
|
1295
|
+
now,
|
|
1296
|
+
hideUsage: true,
|
|
1297
|
+
onSignIn: () => {
|
|
1298
|
+
signIn();
|
|
1299
|
+
}
|
|
1300
|
+
}),
|
|
1301
|
+
rest.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1302
|
+
type: "button",
|
|
1303
|
+
className: "dsm-fold",
|
|
1304
|
+
"aria-expanded": moreOpen,
|
|
1305
|
+
onClick: () => {
|
|
1306
|
+
setMoreOpen((open) => !open);
|
|
1307
|
+
},
|
|
1308
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1309
|
+
className: "dsm-fold-name",
|
|
1310
|
+
children: [
|
|
1311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1312
|
+
name: "right",
|
|
1313
|
+
size: 12,
|
|
1314
|
+
className: "dsm-chev"
|
|
1315
|
+
}),
|
|
1316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: rest.length === 1 ? label(rest[0]) : tf(t, "moreAccounts", { count: rest.length }) }),
|
|
1317
|
+
rest.length === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTags, {
|
|
1318
|
+
t,
|
|
1319
|
+
account: rest[0],
|
|
1320
|
+
status
|
|
1321
|
+
}) : null
|
|
1322
|
+
]
|
|
1323
|
+
})
|
|
1324
|
+
}), moreOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1325
|
+
className: "dsm-fold-body",
|
|
1326
|
+
children: rest.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
1327
|
+
t,
|
|
1328
|
+
provider,
|
|
1329
|
+
account,
|
|
1330
|
+
status,
|
|
1331
|
+
snapshot,
|
|
1332
|
+
prefs: prefView,
|
|
1333
|
+
now,
|
|
1334
|
+
hideUsage: true,
|
|
1335
|
+
onSignIn: () => {
|
|
1336
|
+
signIn();
|
|
1337
|
+
}
|
|
1338
|
+
}, account.key))
|
|
1339
|
+
}) : null] }),
|
|
1340
|
+
mode.kind === "manage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagePanel, {
|
|
1341
|
+
t,
|
|
1342
|
+
provider,
|
|
1343
|
+
status,
|
|
1344
|
+
rpc,
|
|
1345
|
+
onClose: () => {
|
|
1346
|
+
setMode({ kind: "view" });
|
|
1347
|
+
store.refreshStatus();
|
|
1348
|
+
},
|
|
1349
|
+
onAddAccount: () => {
|
|
1350
|
+
signIn();
|
|
1351
|
+
}
|
|
1352
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1353
|
+
className: "dsm-foot",
|
|
1354
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1355
|
+
type: "button",
|
|
1356
|
+
className: "dsm-btn",
|
|
1357
|
+
onClick: () => {
|
|
1358
|
+
setMode({ kind: "manage" });
|
|
1359
|
+
},
|
|
1360
|
+
children: t("manage")
|
|
1361
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerCount, {
|
|
1362
|
+
t,
|
|
1363
|
+
status
|
|
1364
|
+
})]
|
|
1365
|
+
}),
|
|
1366
|
+
error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1367
|
+
className: "dsm-card-error",
|
|
1368
|
+
role: "alert",
|
|
1369
|
+
children: error
|
|
1370
|
+
})
|
|
1371
|
+
]
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
const label = (account) => account.account ?? account.key;
|
|
1375
|
+
function usageOf(snapshot, provider, account) {
|
|
1376
|
+
const entry = snapshot.usage.get(usageKey(provider, account));
|
|
1377
|
+
return entry?.kind === "ready" ? entry.usage : void 0;
|
|
1378
|
+
}
|
|
1379
|
+
/** Plan tag, default star, and serving bolt. */
|
|
1380
|
+
function AccountTags({ t, account, status }) {
|
|
1381
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1382
|
+
account.plan === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1383
|
+
className: "dsm-tag",
|
|
1384
|
+
children: account.plan
|
|
1385
|
+
}),
|
|
1386
|
+
account.isDefault ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1387
|
+
className: "dsm-glyph dsm-star",
|
|
1388
|
+
title: t("defaultAccount"),
|
|
1389
|
+
"aria-label": t("defaultAccount"),
|
|
1390
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1391
|
+
name: "star",
|
|
1392
|
+
size: 12
|
|
1393
|
+
})
|
|
1394
|
+
}) : null,
|
|
1395
|
+
status.servingAccount === account.key ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1396
|
+
className: "dsm-glyph dsm-live",
|
|
1397
|
+
title: t("servingAccount"),
|
|
1398
|
+
"aria-label": t("servingAccount"),
|
|
1399
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1400
|
+
name: "bolt",
|
|
1401
|
+
size: 12
|
|
1402
|
+
})
|
|
1403
|
+
}) : null
|
|
1404
|
+
] });
|
|
1405
|
+
}
|
|
1406
|
+
/** "live", "4m ago", or a stale warning, at the end of an account header. */
|
|
1407
|
+
function Freshness({ t, snapshot, provider, account, now }) {
|
|
1408
|
+
if (!isMetered(snapshot.status, provider)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1409
|
+
className: "dsm-fresh",
|
|
1410
|
+
title: t("noUsageDataTitle"),
|
|
1411
|
+
children: t("noUsageData")
|
|
1412
|
+
});
|
|
1413
|
+
const entry = snapshot.usage.get(usageKey(provider, account));
|
|
1414
|
+
if (entry === void 0 || entry.kind === "loading") return null;
|
|
1415
|
+
if (entry.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1416
|
+
className: "dsm-fresh dsm-stale",
|
|
1417
|
+
title: entry.message,
|
|
1418
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1419
|
+
name: "alert",
|
|
1420
|
+
size: 12
|
|
1421
|
+
}), t("usageUnavailable")]
|
|
1422
|
+
});
|
|
1423
|
+
const usage = entry.usage;
|
|
1424
|
+
const fetchedAt = usage.fetchedAt ?? entry.receivedAt;
|
|
1425
|
+
if (usage.refreshFailed === true || usage.stale === true && usage.retryAt !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1426
|
+
className: "dsm-fresh dsm-stale",
|
|
1427
|
+
title: tf(t, "staleTitle", { when: usage.retryAt === void 0 ? "" : tf(t, "resetsIn", { time: shortDuration(Math.max(0, usage.retryAt - now)) }) }),
|
|
1428
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1429
|
+
name: "alert",
|
|
1430
|
+
size: 12
|
|
1431
|
+
}), tf(t, "staleShort", { time: shortDuration(Math.max(0, now - fetchedAt)) })]
|
|
1432
|
+
});
|
|
1433
|
+
if (usage.source === "live" && now - fetchedAt < 10 * 6e4) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1434
|
+
className: "dsm-fresh",
|
|
1435
|
+
title: t("liveTitle"),
|
|
1436
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-live-dot" }), t("live")]
|
|
1437
|
+
});
|
|
1438
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1439
|
+
className: "dsm-fresh",
|
|
1440
|
+
children: tf(t, "agoShort", { time: shortDuration(Math.max(0, now - fetchedAt)) })
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
/** One account: header, then its windows on the shared grid. */
|
|
1444
|
+
function Account({ t, provider, account, status, snapshot, prefs, now, onSignIn, compact, hideUsage }) {
|
|
1445
|
+
const usage = usageOf(snapshot, provider, account.key);
|
|
1446
|
+
const expired = usage === void 0 && snapshot.usage.get(usageKey(provider, account.key))?.kind === "error" && account.expiresAt !== void 0 && account.expiresAt < now;
|
|
1447
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1448
|
+
className: compact === true ? "dsm-pacct" : "dsm-acct",
|
|
1449
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1450
|
+
className: "dsm-acct-head",
|
|
1451
|
+
children: [
|
|
1452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1453
|
+
className: "dsm-acct-name",
|
|
1454
|
+
children: label(account)
|
|
1455
|
+
}),
|
|
1456
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTags, {
|
|
1457
|
+
t,
|
|
1458
|
+
account,
|
|
1459
|
+
status
|
|
1460
|
+
}),
|
|
1461
|
+
hideUsage === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Freshness, {
|
|
1462
|
+
t,
|
|
1463
|
+
snapshot,
|
|
1464
|
+
provider,
|
|
1465
|
+
account: account.key,
|
|
1466
|
+
now
|
|
1467
|
+
})
|
|
1468
|
+
]
|
|
1469
|
+
}), expired && onSignIn !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1470
|
+
className: "dsm-alert",
|
|
1471
|
+
role: "alert",
|
|
1472
|
+
children: [
|
|
1473
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "alert" }),
|
|
1474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("signInExpired") }),
|
|
1475
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1476
|
+
type: "button",
|
|
1477
|
+
className: "dsm-btn dsm-primary",
|
|
1478
|
+
onClick: onSignIn,
|
|
1479
|
+
children: t("signInAgain")
|
|
1480
|
+
})
|
|
1481
|
+
]
|
|
1482
|
+
}) : hideUsage === true || usage?.windows === void 0 || usage.windows.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1483
|
+
className: "dsm-wins",
|
|
1484
|
+
children: shownWindows(usage.windows).map((window$1, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WindowRow, {
|
|
1485
|
+
t,
|
|
1486
|
+
window: window$1,
|
|
1487
|
+
display: prefs.display,
|
|
1488
|
+
now
|
|
1489
|
+
}, index))
|
|
1490
|
+
})]
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
function PickerCount({ t, status }) {
|
|
1494
|
+
const total = status.catalogSize;
|
|
1495
|
+
if (total === void 0) return null;
|
|
1496
|
+
const visible = status.visibleModels === void 0 ? total : status.visibleModels.length;
|
|
1497
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1498
|
+
className: "dsm-muted dsm-num",
|
|
1499
|
+
title: t("modelsCountTitle"),
|
|
1500
|
+
children: tf(t, "modelsCount", {
|
|
1501
|
+
visible,
|
|
1502
|
+
total
|
|
1503
|
+
})
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function SignIn({ t, provider, mode, rpc, onCancel, onError }) {
|
|
1507
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
1508
|
+
if (mode.userCode !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1509
|
+
className: "dsm-signin",
|
|
1510
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
|
|
1511
|
+
className: "dsm-steps",
|
|
1512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1513
|
+
"data-s": "done",
|
|
1514
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1515
|
+
name: "check",
|
|
1516
|
+
size: 12
|
|
1517
|
+
}), t("stepCodeIssued")]
|
|
1518
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1519
|
+
"data-s": "now",
|
|
1520
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-spin" }), t("stepWaitingGithub")]
|
|
1521
|
+
})]
|
|
1522
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1523
|
+
className: "dsm-foot",
|
|
1524
|
+
style: { flexWrap: "wrap" },
|
|
1525
|
+
children: [
|
|
1526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1527
|
+
className: "dsm-code",
|
|
1528
|
+
children: mode.userCode
|
|
1529
|
+
}),
|
|
1530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1531
|
+
type: "button",
|
|
1532
|
+
className: "dsm-btn dsm-icon",
|
|
1533
|
+
"aria-label": t("copyCode"),
|
|
1534
|
+
title: t("copyCode"),
|
|
1535
|
+
onClick: () => {
|
|
1536
|
+
navigator.clipboard?.writeText(mode.userCode ?? "");
|
|
1537
|
+
},
|
|
1538
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "copy" })
|
|
1539
|
+
}),
|
|
1540
|
+
mode.url === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1541
|
+
className: "dsm-btn",
|
|
1542
|
+
href: mode.url,
|
|
1543
|
+
target: "_blank",
|
|
1544
|
+
rel: "noopener noreferrer",
|
|
1545
|
+
children: [new URL(mode.url).host + new URL(mode.url).pathname, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1546
|
+
name: "ext",
|
|
1547
|
+
size: 12
|
|
1548
|
+
})]
|
|
1549
|
+
}),
|
|
1550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1551
|
+
type: "button",
|
|
1552
|
+
className: "dsm-btn dsm-ghost",
|
|
1553
|
+
style: { marginLeft: "auto" },
|
|
1554
|
+
onClick: onCancel,
|
|
1555
|
+
children: t("cancel")
|
|
1556
|
+
})
|
|
1557
|
+
]
|
|
1558
|
+
})]
|
|
1559
|
+
});
|
|
1560
|
+
const submit = async () => {
|
|
1561
|
+
const input = draft.trim();
|
|
1562
|
+
if (input === "") return;
|
|
1563
|
+
onError(void 0);
|
|
1564
|
+
try {
|
|
1565
|
+
await call(rpc, "manual", {
|
|
1566
|
+
provider,
|
|
1567
|
+
input
|
|
1568
|
+
});
|
|
1569
|
+
setDraft("");
|
|
1570
|
+
} catch (failure) {
|
|
1571
|
+
onError(failure instanceof Error ? failure.message : String(failure));
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1575
|
+
className: "dsm-signin",
|
|
1576
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
|
|
1577
|
+
className: "dsm-steps",
|
|
1578
|
+
children: [
|
|
1579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1580
|
+
"data-s": "done",
|
|
1581
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1582
|
+
name: "check",
|
|
1583
|
+
size: 12
|
|
1584
|
+
}), t("stepTabOpened")]
|
|
1585
|
+
}),
|
|
1586
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1587
|
+
"data-s": "now",
|
|
1588
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-spin" }), t("stepApprove")]
|
|
1589
|
+
}),
|
|
1590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1591
|
+
name: "check",
|
|
1592
|
+
size: 12
|
|
1593
|
+
}), t("stepLoadModels")] })
|
|
1594
|
+
]
|
|
1595
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1596
|
+
className: "dsm-foot",
|
|
1597
|
+
style: { alignItems: "flex-start" },
|
|
1598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1599
|
+
className: "dsm-fallback",
|
|
1600
|
+
style: { flex: 1 },
|
|
1601
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
1602
|
+
name: "down",
|
|
1603
|
+
size: 12
|
|
1604
|
+
}), t("pasteRedirect")] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1605
|
+
className: "dsm-foot",
|
|
1606
|
+
onSubmit: (event) => {
|
|
1607
|
+
event.preventDefault();
|
|
1608
|
+
submit();
|
|
1609
|
+
},
|
|
1610
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1611
|
+
className: "dsm-input",
|
|
1612
|
+
value: draft,
|
|
1613
|
+
placeholder: t("pastePlaceholder"),
|
|
1614
|
+
onChange: (event) => {
|
|
1615
|
+
setDraft(event.target.value);
|
|
1616
|
+
}
|
|
1617
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1618
|
+
type: "submit",
|
|
1619
|
+
className: "dsm-btn",
|
|
1620
|
+
disabled: draft.trim() === "",
|
|
1621
|
+
children: t("continue")
|
|
1622
|
+
})]
|
|
1623
|
+
})]
|
|
1624
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1625
|
+
type: "button",
|
|
1626
|
+
className: "dsm-btn dsm-ghost",
|
|
1627
|
+
onClick: onCancel,
|
|
1628
|
+
children: t("cancel")
|
|
1629
|
+
})]
|
|
1630
|
+
})]
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
const TOOL_LABEL = {
|
|
1634
|
+
web_search: "toolWebSearch",
|
|
1635
|
+
image_generate: "toolImage",
|
|
1636
|
+
video_generate: "toolVideo",
|
|
1637
|
+
x_search: "toolXSearch"
|
|
1638
|
+
};
|
|
1639
|
+
function ManagePanel({ t, provider, status, rpc, onClose, onAddAccount }) {
|
|
1640
|
+
const [tab, setTab] = (0, react.useState)("accounts");
|
|
1641
|
+
const [loaded, setLoaded] = (0, react.useState)();
|
|
1642
|
+
const [draft, setDraft] = (0, react.useState)();
|
|
1643
|
+
const [efforts, setEfforts] = (0, react.useState)({});
|
|
1644
|
+
const [search, setSearch] = (0, react.useState)("");
|
|
1645
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
1646
|
+
const [error, setError] = (0, react.useState)();
|
|
1647
|
+
(0, react.useEffect)(() => {
|
|
1648
|
+
let live = true;
|
|
1649
|
+
call(rpc, "providerSettings", { provider }).then((value) => {
|
|
1650
|
+
if (!live) return;
|
|
1651
|
+
setLoaded(value);
|
|
1652
|
+
setDraft(value.settings);
|
|
1653
|
+
}, (failure) => {
|
|
1654
|
+
if (live) setError(tf(t, "loadFailed", { message: failure instanceof Error ? failure.message : String(failure) }));
|
|
1655
|
+
});
|
|
1656
|
+
return () => {
|
|
1657
|
+
live = false;
|
|
1658
|
+
};
|
|
1659
|
+
}, [
|
|
1660
|
+
rpc,
|
|
1661
|
+
provider,
|
|
1662
|
+
t
|
|
1663
|
+
]);
|
|
1664
|
+
const accountAction = async (endpoint, account) => {
|
|
1665
|
+
setError(void 0);
|
|
1666
|
+
try {
|
|
1667
|
+
await call(rpc, endpoint, {
|
|
1668
|
+
provider,
|
|
1669
|
+
account
|
|
1670
|
+
});
|
|
1671
|
+
onClose();
|
|
1672
|
+
} catch (failure) {
|
|
1673
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
const save = async () => {
|
|
1677
|
+
if (draft === void 0 || loaded === void 0) return;
|
|
1678
|
+
setSaving(true);
|
|
1679
|
+
setError(void 0);
|
|
1680
|
+
try {
|
|
1681
|
+
await call(rpc, "setProviderSettings", {
|
|
1682
|
+
provider,
|
|
1683
|
+
settings: draft
|
|
1684
|
+
});
|
|
1685
|
+
for (const [model, effort] of Object.entries(efforts)) await call(rpc, "setModelDefault", {
|
|
1686
|
+
provider,
|
|
1687
|
+
model,
|
|
1688
|
+
...effort === void 0 ? {} : { effort }
|
|
1689
|
+
});
|
|
1690
|
+
onClose();
|
|
1691
|
+
} catch (failure) {
|
|
1692
|
+
setError(tf(t, "saveFailed", { message: failure instanceof Error ? failure.message : String(failure) }));
|
|
1693
|
+
} finally {
|
|
1694
|
+
setSaving(false);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
const models = loaded?.models ?? [];
|
|
1698
|
+
const allIds = models.map((m) => m.id);
|
|
1699
|
+
const visible = new Set(draft?.visibleModels ?? allIds);
|
|
1700
|
+
const automatic = draft?.visibleModels === void 0;
|
|
1701
|
+
const setVisible = (ids) => {
|
|
1702
|
+
setDraft((current) => {
|
|
1703
|
+
const next = { ...current };
|
|
1704
|
+
if (ids === void 0) delete next.visibleModels;
|
|
1705
|
+
else next.visibleModels = ids;
|
|
1706
|
+
return next;
|
|
1707
|
+
});
|
|
1708
|
+
};
|
|
1709
|
+
const accountPrefs = draft?.accounts ?? {};
|
|
1710
|
+
const setPool = (account, enabled) => {
|
|
1711
|
+
setDraft((current) => ({
|
|
1712
|
+
...current,
|
|
1713
|
+
accounts: {
|
|
1714
|
+
...current?.accounts,
|
|
1715
|
+
[account]: {
|
|
1716
|
+
...current?.accounts?.[account],
|
|
1717
|
+
poolEnabled: enabled
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}));
|
|
1721
|
+
};
|
|
1722
|
+
const toolsOn = draft?.tools ?? {};
|
|
1723
|
+
const shown = models.filter((m) => search === "" || m.id.toLowerCase().includes(search.toLowerCase()) || m.name.toLowerCase().includes(search.toLowerCase()));
|
|
1724
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1725
|
+
className: "dsm-panel",
|
|
1726
|
+
children: [
|
|
1727
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1728
|
+
className: "dsm-tabs",
|
|
1729
|
+
role: "tablist",
|
|
1730
|
+
children: [
|
|
1731
|
+
"accounts",
|
|
1732
|
+
"models",
|
|
1733
|
+
"tools"
|
|
1734
|
+
].filter((id) => id !== "tools" || (loaded?.tools.length ?? 0) > 0).map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1735
|
+
type: "button",
|
|
1736
|
+
role: "tab",
|
|
1737
|
+
className: "dsm-tab",
|
|
1738
|
+
"aria-selected": tab === id,
|
|
1739
|
+
onClick: () => {
|
|
1740
|
+
setTab(id);
|
|
1741
|
+
},
|
|
1742
|
+
children: t(id === "accounts" ? "tabAccounts" : id === "models" ? "tabModels" : "tabTools")
|
|
1743
|
+
}, id))
|
|
1744
|
+
}),
|
|
1745
|
+
tab === "accounts" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [status.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1746
|
+
className: "dsm-row",
|
|
1747
|
+
children: [
|
|
1748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1749
|
+
className: "dsm-grow",
|
|
1750
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1751
|
+
className: "dsm-ellipsis",
|
|
1752
|
+
children: label(account)
|
|
1753
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTags, {
|
|
1754
|
+
t,
|
|
1755
|
+
account,
|
|
1756
|
+
status
|
|
1757
|
+
})]
|
|
1758
|
+
}),
|
|
1759
|
+
status.accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1760
|
+
className: "dsm-note",
|
|
1761
|
+
children: t("pool")
|
|
1762
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1763
|
+
type: "button",
|
|
1764
|
+
role: "switch",
|
|
1765
|
+
className: "dsm-switch",
|
|
1766
|
+
"aria-checked": accountPrefs[account.key]?.poolEnabled !== false,
|
|
1767
|
+
"aria-label": tf(t, "poolInclude", { account: label(account) }),
|
|
1768
|
+
onClick: () => {
|
|
1769
|
+
setPool(account.key, accountPrefs[account.key]?.poolEnabled === false);
|
|
1770
|
+
}
|
|
1771
|
+
})] }) : null,
|
|
1772
|
+
account.isDefault ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1773
|
+
type: "button",
|
|
1774
|
+
className: "dsm-btn dsm-icon",
|
|
1775
|
+
title: t("makeDefault"),
|
|
1776
|
+
"aria-label": t("makeDefault"),
|
|
1777
|
+
onClick: () => {
|
|
1778
|
+
accountAction("setDefault", account.key);
|
|
1779
|
+
},
|
|
1780
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "star" })
|
|
1781
|
+
}),
|
|
1782
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1783
|
+
type: "button",
|
|
1784
|
+
className: "dsm-btn dsm-danger",
|
|
1785
|
+
onClick: () => {
|
|
1786
|
+
accountAction("logout", account.key);
|
|
1787
|
+
},
|
|
1788
|
+
children: t("signOut")
|
|
1789
|
+
})
|
|
1790
|
+
]
|
|
1791
|
+
}, account.key)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1792
|
+
className: "dsm-row",
|
|
1793
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1794
|
+
type: "button",
|
|
1795
|
+
className: "dsm-btn",
|
|
1796
|
+
onClick: onAddAccount,
|
|
1797
|
+
children: ["+ ", t("addAccount")]
|
|
1798
|
+
})
|
|
1799
|
+
})] }) : null,
|
|
1800
|
+
tab === "models" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1802
|
+
className: "dsm-input",
|
|
1803
|
+
value: search,
|
|
1804
|
+
placeholder: tf(t, "searchModels", { count: models.length }),
|
|
1805
|
+
onChange: (event) => {
|
|
1806
|
+
setSearch(event.target.value);
|
|
1807
|
+
}
|
|
1808
|
+
}),
|
|
1809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1810
|
+
className: "dsm-row",
|
|
1811
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1812
|
+
className: "dsm-grow",
|
|
1813
|
+
children: t("autoShowNew")
|
|
1814
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1815
|
+
type: "button",
|
|
1816
|
+
role: "switch",
|
|
1817
|
+
className: "dsm-switch",
|
|
1818
|
+
"aria-checked": automatic,
|
|
1819
|
+
"aria-label": t("autoShowNew"),
|
|
1820
|
+
onClick: () => {
|
|
1821
|
+
setVisible(automatic ? allIds : void 0);
|
|
1822
|
+
}
|
|
1823
|
+
})]
|
|
1824
|
+
}),
|
|
1825
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1826
|
+
className: "dsm-row",
|
|
1827
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1828
|
+
type: "button",
|
|
1829
|
+
className: "dsm-btn",
|
|
1830
|
+
onClick: () => {
|
|
1831
|
+
setVisible(allIds);
|
|
1832
|
+
},
|
|
1833
|
+
children: t("selectAll")
|
|
1834
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1835
|
+
type: "button",
|
|
1836
|
+
className: "dsm-btn",
|
|
1837
|
+
onClick: () => {
|
|
1838
|
+
setVisible([]);
|
|
1839
|
+
},
|
|
1840
|
+
children: t("deselectAll")
|
|
1841
|
+
})]
|
|
1842
|
+
}),
|
|
1843
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1844
|
+
className: "dsm-list",
|
|
1845
|
+
children: shown.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
|
|
1846
|
+
t,
|
|
1847
|
+
model,
|
|
1848
|
+
on: visible.has(model.id),
|
|
1849
|
+
effort: model.id in efforts ? efforts[model.id] : model.configured,
|
|
1850
|
+
onToggle: () => {
|
|
1851
|
+
const next = new Set(visible);
|
|
1852
|
+
if (next.has(model.id)) next.delete(model.id);
|
|
1853
|
+
else next.add(model.id);
|
|
1854
|
+
setVisible(allIds.filter((id) => next.has(id)));
|
|
1855
|
+
},
|
|
1856
|
+
onEffort: (effort) => {
|
|
1857
|
+
setEfforts((current) => ({
|
|
1858
|
+
...current,
|
|
1859
|
+
[model.id]: effort
|
|
1860
|
+
}));
|
|
1861
|
+
}
|
|
1862
|
+
}, model.id))
|
|
1863
|
+
}),
|
|
1864
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1865
|
+
className: "dsm-note",
|
|
1866
|
+
children: tf(t, "pickerCount", {
|
|
1867
|
+
visible: visible.size,
|
|
1868
|
+
total: models.length
|
|
1869
|
+
})
|
|
1870
|
+
})
|
|
1871
|
+
] }) : null,
|
|
1872
|
+
tab === "tools" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(loaded?.tools ?? []).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1873
|
+
className: "dsm-row",
|
|
1874
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1875
|
+
className: "dsm-grow",
|
|
1876
|
+
children: t(TOOL_LABEL[tool])
|
|
1877
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1878
|
+
type: "button",
|
|
1879
|
+
role: "switch",
|
|
1880
|
+
className: "dsm-switch",
|
|
1881
|
+
"aria-checked": toolsOn[tool] !== false,
|
|
1882
|
+
"aria-label": t(TOOL_LABEL[tool]),
|
|
1883
|
+
onClick: () => {
|
|
1884
|
+
setDraft((current) => ({
|
|
1885
|
+
...current,
|
|
1886
|
+
tools: {
|
|
1887
|
+
...current?.tools,
|
|
1888
|
+
[tool]: current?.tools?.[tool] === false
|
|
1889
|
+
}
|
|
1890
|
+
}));
|
|
1891
|
+
}
|
|
1892
|
+
})]
|
|
1893
|
+
}, tool)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1894
|
+
className: "dsm-note",
|
|
1895
|
+
children: t("toolsNote")
|
|
1896
|
+
})] }) : null,
|
|
1897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1898
|
+
className: "dsm-panel-foot",
|
|
1899
|
+
children: [
|
|
1900
|
+
error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1901
|
+
className: "dsm-card-error",
|
|
1902
|
+
role: "alert",
|
|
1903
|
+
children: error
|
|
1904
|
+
}),
|
|
1905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1906
|
+
type: "button",
|
|
1907
|
+
className: "dsm-btn dsm-ghost",
|
|
1908
|
+
onClick: onClose,
|
|
1909
|
+
children: t("cancel")
|
|
1910
|
+
}),
|
|
1911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1912
|
+
type: "button",
|
|
1913
|
+
className: "dsm-btn dsm-primary",
|
|
1914
|
+
disabled: saving || draft === void 0,
|
|
1915
|
+
onClick: () => {
|
|
1916
|
+
save();
|
|
1917
|
+
},
|
|
1918
|
+
children: saving ? t("saving") : t("save")
|
|
1919
|
+
})
|
|
1920
|
+
]
|
|
1921
|
+
})
|
|
1922
|
+
]
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function ModelRow({ t, model, on, effort, onToggle, onEffort }) {
|
|
1926
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1927
|
+
className: "dsm-row",
|
|
1928
|
+
children: [
|
|
1929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1930
|
+
type: "button",
|
|
1931
|
+
role: "switch",
|
|
1932
|
+
className: "dsm-switch",
|
|
1933
|
+
"aria-checked": on,
|
|
1934
|
+
"aria-label": tf(t, "showInPicker", { model: model.id }),
|
|
1935
|
+
onClick: onToggle
|
|
1936
|
+
}),
|
|
1937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1938
|
+
className: "dsm-grow",
|
|
1939
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1940
|
+
className: "dsm-ellipsis dsm-num",
|
|
1941
|
+
children: model.id
|
|
1942
|
+
}), model.contextWindow === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1943
|
+
className: "dsm-note",
|
|
1944
|
+
children: [Math.round(model.contextWindow / 1e3), "K"]
|
|
1945
|
+
})]
|
|
1946
|
+
}),
|
|
1947
|
+
model.efforts.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1948
|
+
className: "dsm-select",
|
|
1949
|
+
"aria-label": tf(t, "effortFor", { model: model.id }),
|
|
1950
|
+
value: effort ?? "",
|
|
1951
|
+
onChange: (event) => {
|
|
1952
|
+
onEffort(event.target.value === "" ? void 0 : event.target.value);
|
|
1953
|
+
},
|
|
1954
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1955
|
+
value: "",
|
|
1956
|
+
children: t("effortAuto")
|
|
1957
|
+
}), model.efforts.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1958
|
+
value: level.id,
|
|
1959
|
+
children: level.name
|
|
1960
|
+
}, level.id))]
|
|
1961
|
+
})
|
|
1962
|
+
]
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
//#endregion
|
|
1967
|
+
//#region src/client/Footer.tsx
|
|
1968
|
+
function Footer(props) {
|
|
1969
|
+
const { store, prefs, rpc, t } = props;
|
|
1970
|
+
if (store === void 0 || prefs === void 0 || rpc === void 0 || t === void 0) return null;
|
|
1971
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FooterBody, {
|
|
1972
|
+
rpc,
|
|
1973
|
+
t
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
function FooterBody({ rpc, t }) {
|
|
1977
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1978
|
+
className: "dsm-footer-block",
|
|
1979
|
+
"aria-labelledby": "dsm-footer-title",
|
|
1980
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1981
|
+
id: "dsm-footer-title",
|
|
1982
|
+
children: t("subscriptions")
|
|
1983
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxyRow, {
|
|
1984
|
+
t,
|
|
1985
|
+
rpc
|
|
1986
|
+
})]
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
function ProxyRow({ t, rpc }) {
|
|
1990
|
+
const [config, setConfig] = (0, react.useState)();
|
|
1991
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1992
|
+
const [url, setUrl] = (0, react.useState)("");
|
|
1993
|
+
const [bypass, setBypass] = (0, react.useState)("");
|
|
1994
|
+
const [enabled, setEnabled] = (0, react.useState)(false);
|
|
1995
|
+
const [message, setMessage] = (0, react.useState)();
|
|
1996
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1997
|
+
(0, react.useEffect)(() => {
|
|
1998
|
+
let live = true;
|
|
1999
|
+
call(rpc, "proxyGet", {}).then((value) => {
|
|
2000
|
+
if (live) setConfig(value);
|
|
2001
|
+
}, () => void 0);
|
|
2002
|
+
return () => {
|
|
2003
|
+
live = false;
|
|
2004
|
+
};
|
|
2005
|
+
}, [rpc]);
|
|
2006
|
+
const edit = () => {
|
|
2007
|
+
setUrl(config?.url ?? "");
|
|
2008
|
+
setBypass((config?.bypass ?? []).join(", "));
|
|
2009
|
+
setEnabled(config?.enabled ?? false);
|
|
2010
|
+
setMessage(void 0);
|
|
2011
|
+
setOpen(true);
|
|
2012
|
+
};
|
|
2013
|
+
const save = async () => {
|
|
2014
|
+
setBusy(true);
|
|
2015
|
+
setMessage(void 0);
|
|
2016
|
+
try {
|
|
2017
|
+
setConfig(await call(rpc, "proxySet", {
|
|
2018
|
+
enabled,
|
|
2019
|
+
url: url.trim(),
|
|
2020
|
+
bypass: bypass.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "")
|
|
2021
|
+
}));
|
|
2022
|
+
setOpen(false);
|
|
2023
|
+
} catch (failure) {
|
|
2024
|
+
setMessage(tf(t, "saveFailed", { message: failure instanceof Error ? failure.message : String(failure) }));
|
|
2025
|
+
} finally {
|
|
2026
|
+
setBusy(false);
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
const test = async () => {
|
|
2030
|
+
setBusy(true);
|
|
2031
|
+
setMessage(void 0);
|
|
2032
|
+
try {
|
|
2033
|
+
const result = await call(rpc, "proxyTest", url.trim() === "" ? {} : { proxy: { url: url.trim() } });
|
|
2034
|
+
setMessage(result.ok && result.status !== void 0 ? tf(t, "proxyTestOk", {
|
|
2035
|
+
status: result.status,
|
|
2036
|
+
ms: result.latencyMs ?? 0
|
|
2037
|
+
}) : result.error ?? "");
|
|
2038
|
+
} catch (failure) {
|
|
2039
|
+
setMessage(failure instanceof Error ? failure.message : String(failure));
|
|
2040
|
+
} finally {
|
|
2041
|
+
setBusy(false);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2045
|
+
className: "dsm-row",
|
|
2046
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2047
|
+
className: "dsm-grow",
|
|
2048
|
+
children: [
|
|
2049
|
+
t("proxy"),
|
|
2050
|
+
" ",
|
|
2051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2052
|
+
className: "dsm-note",
|
|
2053
|
+
children: config?.enabled === true ? config.url : t("proxyOff")
|
|
2054
|
+
})
|
|
2055
|
+
]
|
|
2056
|
+
}), open ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2057
|
+
type: "button",
|
|
2058
|
+
className: "dsm-btn",
|
|
2059
|
+
onClick: edit,
|
|
2060
|
+
children: t("configure")
|
|
2061
|
+
})]
|
|
2062
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2063
|
+
className: "dsm-panel",
|
|
2064
|
+
style: { paddingTop: 12 },
|
|
2065
|
+
children: [
|
|
2066
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2067
|
+
className: "dsm-row",
|
|
2068
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2069
|
+
className: "dsm-grow",
|
|
2070
|
+
children: t("proxyEnabled")
|
|
2071
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2072
|
+
type: "button",
|
|
2073
|
+
role: "switch",
|
|
2074
|
+
className: "dsm-switch",
|
|
2075
|
+
"aria-checked": enabled,
|
|
2076
|
+
"aria-label": t("proxyEnabled"),
|
|
2077
|
+
onClick: () => {
|
|
2078
|
+
setEnabled((value) => !value);
|
|
2079
|
+
}
|
|
2080
|
+
})]
|
|
2081
|
+
}),
|
|
2082
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2083
|
+
className: "dsm-input",
|
|
2084
|
+
value: url,
|
|
2085
|
+
placeholder: "http://127.0.0.1:7890",
|
|
2086
|
+
"aria-label": t("proxyUrl"),
|
|
2087
|
+
onChange: (event) => {
|
|
2088
|
+
setUrl(event.target.value);
|
|
2089
|
+
}
|
|
2090
|
+
}),
|
|
2091
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2092
|
+
className: "dsm-input",
|
|
2093
|
+
value: bypass,
|
|
2094
|
+
placeholder: t("proxyBypass"),
|
|
2095
|
+
"aria-label": t("proxyBypass"),
|
|
2096
|
+
onChange: (event) => {
|
|
2097
|
+
setBypass(event.target.value);
|
|
2098
|
+
}
|
|
2099
|
+
}),
|
|
2100
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2101
|
+
className: "dsm-panel-foot",
|
|
2102
|
+
children: [
|
|
2103
|
+
message === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2104
|
+
className: "dsm-note",
|
|
2105
|
+
role: "status",
|
|
2106
|
+
style: { marginRight: "auto" },
|
|
2107
|
+
children: message
|
|
2108
|
+
}),
|
|
2109
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2110
|
+
type: "button",
|
|
2111
|
+
className: "dsm-btn dsm-ghost",
|
|
2112
|
+
disabled: busy,
|
|
2113
|
+
onClick: () => {
|
|
2114
|
+
test();
|
|
2115
|
+
},
|
|
2116
|
+
children: t("proxyTest")
|
|
2117
|
+
}),
|
|
2118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2119
|
+
type: "button",
|
|
2120
|
+
className: "dsm-btn dsm-ghost",
|
|
2121
|
+
onClick: () => {
|
|
2122
|
+
setOpen(false);
|
|
2123
|
+
},
|
|
2124
|
+
children: t("cancel")
|
|
2125
|
+
}),
|
|
2126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2127
|
+
type: "button",
|
|
2128
|
+
className: "dsm-btn dsm-primary",
|
|
2129
|
+
disabled: busy,
|
|
2130
|
+
onClick: () => {
|
|
2131
|
+
save();
|
|
2132
|
+
},
|
|
2133
|
+
children: t("save")
|
|
2134
|
+
})
|
|
2135
|
+
]
|
|
2136
|
+
})
|
|
2137
|
+
]
|
|
2138
|
+
}) : null] });
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
//#endregion
|
|
2142
|
+
//#region src/client/Usage.tsx
|
|
2143
|
+
/** The Usage row's icon in the settings nav; the shell passes the edge size. */
|
|
2144
|
+
function UsageNavIcon({ size }) {
|
|
2145
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2146
|
+
name: "meter",
|
|
2147
|
+
size
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
function Usage(props) {
|
|
2151
|
+
const { store, prefs, rpc, t } = props;
|
|
2152
|
+
if (store === void 0 || prefs === void 0 || rpc === void 0 || t === void 0) return null;
|
|
2153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBody, {
|
|
2154
|
+
store,
|
|
2155
|
+
prefs,
|
|
2156
|
+
rpc,
|
|
2157
|
+
t
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
function Segmented({ label: label$1, value, options, disabled, onChange }) {
|
|
2161
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2162
|
+
className: "dsm-seg",
|
|
2163
|
+
role: "group",
|
|
2164
|
+
"aria-label": label$1,
|
|
2165
|
+
children: options.map(([option, text]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2166
|
+
type: "button",
|
|
2167
|
+
"aria-pressed": value === option,
|
|
2168
|
+
disabled,
|
|
2169
|
+
onClick: () => {
|
|
2170
|
+
onChange(option);
|
|
2171
|
+
},
|
|
2172
|
+
children: text
|
|
2173
|
+
}, option))
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
function maxManualReady(snapshot, provider, status) {
|
|
2177
|
+
const times = status.accounts.map((a) => {
|
|
2178
|
+
const entry = snapshot.usage.get(usageKey(provider, a.key));
|
|
2179
|
+
return entry?.kind === "ready" ? entry.usage.manualReadyAt : void 0;
|
|
2180
|
+
}).filter((at) => at !== void 0);
|
|
2181
|
+
return times.length === 0 ? void 0 : Math.max(...times);
|
|
2182
|
+
}
|
|
2183
|
+
function UsageBody({ store, prefs, rpc, t }) {
|
|
2184
|
+
const snapshot = useStore(store);
|
|
2185
|
+
const view = usePrefs(prefs);
|
|
2186
|
+
const now = useNow(3e4);
|
|
2187
|
+
const readOnly = !view.writable;
|
|
2188
|
+
const providers = USAGE_PROVIDERS.filter((provider) => {
|
|
2189
|
+
const status = snapshot.status?.providers[provider];
|
|
2190
|
+
return status !== void 0 && status.accounts.length > 0 && isMetered(snapshot.status, provider);
|
|
2191
|
+
});
|
|
2192
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2193
|
+
className: "dsm-footer-block dsm-usage",
|
|
2194
|
+
"aria-labelledby": "dsm-usage-title",
|
|
2195
|
+
style: {
|
|
2196
|
+
marginTop: 0,
|
|
2197
|
+
borderTop: "none",
|
|
2198
|
+
paddingTop: 0
|
|
2199
|
+
},
|
|
2200
|
+
children: [
|
|
2201
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2202
|
+
id: "dsm-usage-title",
|
|
2203
|
+
children: t("usage")
|
|
2204
|
+
}),
|
|
2205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2206
|
+
className: "dsm-row",
|
|
2207
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2208
|
+
className: "dsm-grow",
|
|
2209
|
+
children: t("showQuotaAs")
|
|
2210
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Segmented, {
|
|
2211
|
+
label: t("showQuotaAs"),
|
|
2212
|
+
value: view.display,
|
|
2213
|
+
disabled: readOnly,
|
|
2214
|
+
options: [["left", t("displayLeft")], ["used", t("displayUsed")]],
|
|
2215
|
+
onChange: (display) => {
|
|
2216
|
+
prefs.set({ display });
|
|
2217
|
+
}
|
|
2218
|
+
})]
|
|
2219
|
+
}),
|
|
2220
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2221
|
+
className: "dsm-row",
|
|
2222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2223
|
+
className: "dsm-grow",
|
|
2224
|
+
children: t("paceWarnings")
|
|
2225
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2226
|
+
type: "button",
|
|
2227
|
+
role: "switch",
|
|
2228
|
+
className: "dsm-switch",
|
|
2229
|
+
"aria-checked": view.paceWarnings,
|
|
2230
|
+
"aria-label": t("paceWarnings"),
|
|
2231
|
+
disabled: readOnly,
|
|
2232
|
+
onClick: () => {
|
|
2233
|
+
prefs.set({ paceWarnings: !view.paceWarnings });
|
|
2234
|
+
}
|
|
2235
|
+
})]
|
|
2236
|
+
}),
|
|
2237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2238
|
+
className: "dsm-row",
|
|
2239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2240
|
+
className: "dsm-grow",
|
|
2241
|
+
children: [
|
|
2242
|
+
t("composerUsage"),
|
|
2243
|
+
" ",
|
|
2244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2245
|
+
className: "dsm-note",
|
|
2246
|
+
children: t("composerUsageNote")
|
|
2247
|
+
})
|
|
2248
|
+
]
|
|
2249
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2250
|
+
type: "button",
|
|
2251
|
+
role: "switch",
|
|
2252
|
+
className: "dsm-switch",
|
|
2253
|
+
"aria-checked": view.composer,
|
|
2254
|
+
"aria-label": t("composerUsage"),
|
|
2255
|
+
disabled: readOnly,
|
|
2256
|
+
onClick: () => {
|
|
2257
|
+
prefs.set({ composer: !view.composer });
|
|
2258
|
+
}
|
|
2259
|
+
})]
|
|
2260
|
+
}),
|
|
2261
|
+
providers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2262
|
+
className: "dsm-muted",
|
|
2263
|
+
style: { margin: "12px 0 0" },
|
|
2264
|
+
children: t("usageEmpty")
|
|
2265
|
+
}) : null,
|
|
2266
|
+
providers.map((provider) => {
|
|
2267
|
+
const status = snapshot.status?.providers[provider];
|
|
2268
|
+
const split = status === void 0 ? void 0 : splitAccounts(status);
|
|
2269
|
+
if (status === void 0 || split === void 0) return null;
|
|
2270
|
+
const pinned = view.pinned.includes(provider);
|
|
2271
|
+
const pinLabel = pinned ? t("unpin") : t("pin");
|
|
2272
|
+
const refreshing = status.accounts.some((a) => snapshot.refreshing.has(usageKey(provider, a.key)));
|
|
2273
|
+
const readyAt = maxManualReady(snapshot, provider, status);
|
|
2274
|
+
const cooling = readyAt !== void 0 && readyAt > now;
|
|
2275
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2276
|
+
className: "dsm-usage-block",
|
|
2277
|
+
style: {
|
|
2278
|
+
borderTop: ".5px solid var(--dsw-alias-border-l1)",
|
|
2279
|
+
padding: "10px 0"
|
|
2280
|
+
},
|
|
2281
|
+
children: [
|
|
2282
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2283
|
+
className: "dsm-row",
|
|
2284
|
+
style: { minHeight: 28 },
|
|
2285
|
+
children: [
|
|
2286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2287
|
+
className: "dsm-grow",
|
|
2288
|
+
style: { fontWeight: 500 },
|
|
2289
|
+
children: shortName(snapshot.status, provider)
|
|
2290
|
+
}),
|
|
2291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2292
|
+
type: "button",
|
|
2293
|
+
className: "dsm-btn dsm-icon dsm-pin",
|
|
2294
|
+
"aria-pressed": pinned,
|
|
2295
|
+
title: pinLabel,
|
|
2296
|
+
"aria-label": pinLabel,
|
|
2297
|
+
disabled: readOnly,
|
|
2298
|
+
onClick: () => {
|
|
2299
|
+
prefs.togglePinned(provider);
|
|
2300
|
+
},
|
|
2301
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: pinned ? "pinFilled" : "pin" })
|
|
2302
|
+
}),
|
|
2303
|
+
isKey(provider) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2304
|
+
type: "button",
|
|
2305
|
+
className: "dsm-btn dsm-ghost",
|
|
2306
|
+
onClick: () => {
|
|
2307
|
+
call(rpc, "keysRemove", { provider }).then(() => store.refreshStatus());
|
|
2308
|
+
},
|
|
2309
|
+
children: t("removeKey")
|
|
2310
|
+
}) : null,
|
|
2311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2312
|
+
type: "button",
|
|
2313
|
+
className: `dsm-btn dsm-icon${refreshing ? " dsm-spinning" : ""}`,
|
|
2314
|
+
disabled: refreshing || cooling,
|
|
2315
|
+
title: cooling ? tf(t, "refreshedWait", { seconds: Math.ceil((readyAt - now) / 1e3) }) : t("refreshUsage"),
|
|
2316
|
+
"aria-label": tf(t, "refreshUsageNamed", { name: provider }),
|
|
2317
|
+
onClick: () => {
|
|
2318
|
+
store.refreshUsage("manual", provider);
|
|
2319
|
+
},
|
|
2320
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "refresh" })
|
|
2321
|
+
})
|
|
2322
|
+
]
|
|
2323
|
+
}),
|
|
2324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
2325
|
+
t,
|
|
2326
|
+
provider,
|
|
2327
|
+
account: split.active,
|
|
2328
|
+
status,
|
|
2329
|
+
snapshot,
|
|
2330
|
+
prefs: view,
|
|
2331
|
+
now,
|
|
2332
|
+
compact: true
|
|
2333
|
+
}),
|
|
2334
|
+
split.rest.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
2335
|
+
t,
|
|
2336
|
+
provider,
|
|
2337
|
+
account,
|
|
2338
|
+
status,
|
|
2339
|
+
snapshot,
|
|
2340
|
+
prefs: view,
|
|
2341
|
+
now,
|
|
2342
|
+
compact: true
|
|
2343
|
+
}, account.key))
|
|
2344
|
+
]
|
|
2345
|
+
}, provider);
|
|
2346
|
+
}),
|
|
2347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddProvider, {
|
|
2348
|
+
t,
|
|
2349
|
+
store,
|
|
2350
|
+
rpc,
|
|
2351
|
+
unconfigured: KEY_PROVIDERS.filter((id) => snapshot.status?.keyProviders?.find((v) => v.id === id)?.configured !== true),
|
|
2352
|
+
names: snapshot.status?.keyProviders ?? []
|
|
2353
|
+
})
|
|
2354
|
+
]
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
const isKey = (provider) => KEY_PROVIDERS.includes(provider);
|
|
2358
|
+
function AddProvider({ t, store, rpc, unconfigured, names }) {
|
|
2359
|
+
const [picked, setPicked] = (0, react.useState)();
|
|
2360
|
+
const [key, setKey] = (0, react.useState)("");
|
|
2361
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2362
|
+
const [error, setError] = (0, react.useState)();
|
|
2363
|
+
if (unconfigured.length === 0) return null;
|
|
2364
|
+
const save = async (id) => {
|
|
2365
|
+
setBusy(true);
|
|
2366
|
+
setError(void 0);
|
|
2367
|
+
try {
|
|
2368
|
+
await call(rpc, "keysSet", {
|
|
2369
|
+
provider: id,
|
|
2370
|
+
key
|
|
2371
|
+
});
|
|
2372
|
+
setKey("");
|
|
2373
|
+
setPicked(void 0);
|
|
2374
|
+
await store.refreshStatus();
|
|
2375
|
+
} catch (failure) {
|
|
2376
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
2377
|
+
} finally {
|
|
2378
|
+
setBusy(false);
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
const nameOf = (id) => names.find((v) => v.id === id)?.name ?? id;
|
|
2382
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2383
|
+
className: "dsm-usage-block",
|
|
2384
|
+
style: {
|
|
2385
|
+
borderTop: ".5px solid var(--dsw-alias-border-l1)",
|
|
2386
|
+
padding: "10px 0"
|
|
2387
|
+
},
|
|
2388
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2389
|
+
className: "dsm-row",
|
|
2390
|
+
style: { minHeight: 28 },
|
|
2391
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2392
|
+
className: "dsm-grow dsm-muted",
|
|
2393
|
+
children: t("addProvider")
|
|
2394
|
+
}), unconfigured.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2395
|
+
type: "button",
|
|
2396
|
+
className: "dsm-btn",
|
|
2397
|
+
"aria-pressed": picked === id,
|
|
2398
|
+
onClick: () => {
|
|
2399
|
+
setPicked(picked === id ? void 0 : id);
|
|
2400
|
+
setError(void 0);
|
|
2401
|
+
},
|
|
2402
|
+
children: nameOf(id)
|
|
2403
|
+
}, id))]
|
|
2404
|
+
}), picked === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2405
|
+
className: "dsm-panel",
|
|
2406
|
+
style: { paddingTop: 8 },
|
|
2407
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2408
|
+
className: "dsm-input",
|
|
2409
|
+
type: "password",
|
|
2410
|
+
autoComplete: "off",
|
|
2411
|
+
value: key,
|
|
2412
|
+
placeholder: t("apiKey"),
|
|
2413
|
+
"aria-label": `${nameOf(picked)} ${t("apiKey")}`,
|
|
2414
|
+
onChange: (event) => {
|
|
2415
|
+
setKey(event.target.value);
|
|
2416
|
+
}
|
|
2417
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2418
|
+
className: "dsm-panel-foot",
|
|
2419
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2420
|
+
className: "dsm-note",
|
|
2421
|
+
role: "status",
|
|
2422
|
+
style: { marginRight: "auto" },
|
|
2423
|
+
children: busy ? t("keyChecking") : error ?? t("keyHelp")
|
|
2424
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2425
|
+
type: "button",
|
|
2426
|
+
className: "dsm-btn dsm-primary",
|
|
2427
|
+
disabled: busy || key.trim() === "",
|
|
2428
|
+
onClick: () => {
|
|
2429
|
+
save(picked);
|
|
2430
|
+
},
|
|
2431
|
+
children: t("addKey")
|
|
2432
|
+
})]
|
|
2433
|
+
})]
|
|
2434
|
+
})]
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
//#endregion
|
|
2439
|
+
//#region src/client/Gauge.tsx
|
|
2440
|
+
const noSubscribe = () => () => void 0;
|
|
2441
|
+
/** Select the provider string itself, so the snapshot compares by value. */
|
|
2442
|
+
function useCurrentProvider(source) {
|
|
2443
|
+
return (0, react.useSyncExternalStore)(source?.subscribe ?? noSubscribe, () => source?.getSnapshot().current?.provider);
|
|
2444
|
+
}
|
|
2445
|
+
/** A provider the composer can show: signed in and reporting usage. */
|
|
2446
|
+
const showable = (snapshot, id) => isMetered(snapshot.status, id) && (snapshot.status?.providers[id]?.accounts.length ?? 0) > 0;
|
|
2447
|
+
/** Providers the gauge shows: the current model's provider first, then the pinned ones. */
|
|
2448
|
+
function gaugeProviders(prefs, current, snapshot) {
|
|
2449
|
+
if (snapshot.status === void 0) return [];
|
|
2450
|
+
return [...new Set(isUsageProviderId(current) ? [current, ...prefs.pinned] : prefs.pinned)].filter((id) => showable(snapshot, id));
|
|
2451
|
+
}
|
|
2452
|
+
/** Every provider the popover can list: the shown ones first, then the rest in page order. */
|
|
2453
|
+
function popoverProviders(shown, snapshot) {
|
|
2454
|
+
return {
|
|
2455
|
+
shown: [...shown],
|
|
2456
|
+
others: USAGE_PROVIDERS.filter((id) => !shown.includes(id) && showable(snapshot, id))
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function windowsOf(snapshot, provider) {
|
|
2460
|
+
const key = activeAccountKey(snapshot.status?.providers[provider]);
|
|
2461
|
+
if (key === void 0) return void 0;
|
|
2462
|
+
const entry = snapshot.usage.get(usageKey(provider, key));
|
|
2463
|
+
return entry?.kind === "ready" ? entry.usage.windows : void 0;
|
|
2464
|
+
}
|
|
2465
|
+
function Gauge(props) {
|
|
2466
|
+
const { store, prefs, rpc, t, sessionId } = props;
|
|
2467
|
+
if (store === void 0 || prefs === void 0 || rpc === void 0 || t === void 0 || sessionId === void 0) return null;
|
|
2468
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeBody, {
|
|
2469
|
+
...props,
|
|
2470
|
+
store,
|
|
2471
|
+
prefs,
|
|
2472
|
+
rpc,
|
|
2473
|
+
t,
|
|
2474
|
+
sessionId,
|
|
2475
|
+
currentModel: props.currentModel
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
function GaugeBody({ store, prefs, t, sessionId, currentModel, openSettings }) {
|
|
2479
|
+
const snapshot = useStore(store);
|
|
2480
|
+
const prefView = usePrefs(prefs);
|
|
2481
|
+
const current = useCurrentProvider(currentModel);
|
|
2482
|
+
const now = useNow(3e4);
|
|
2483
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2484
|
+
const anchor = (0, react.useRef)(null);
|
|
2485
|
+
(0, react.useEffect)(() => {
|
|
2486
|
+
store.focusSession(sessionId);
|
|
2487
|
+
}, [store, sessionId]);
|
|
2488
|
+
if (!prefView.composer || snapshot.status === void 0) return null;
|
|
2489
|
+
const shown = gaugeProviders(prefView, current, snapshot);
|
|
2490
|
+
if (shown.length === 0 && popoverProviders(shown, snapshot).others.length === 0) return null;
|
|
2491
|
+
const toggle = () => {
|
|
2492
|
+
setOpen((value) => {
|
|
2493
|
+
if (!value) store.refreshUsage("open");
|
|
2494
|
+
return !value;
|
|
2495
|
+
});
|
|
2496
|
+
};
|
|
2497
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2498
|
+
className: "dsm-gauge-slot",
|
|
2499
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2500
|
+
ref: anchor,
|
|
2501
|
+
type: "button",
|
|
2502
|
+
className: "dsm-gauge",
|
|
2503
|
+
"aria-expanded": open,
|
|
2504
|
+
"aria-haspopup": "dialog",
|
|
2505
|
+
"aria-label": t("usageLabel"),
|
|
2506
|
+
title: shown.length === 0 ? t("usageLabel") : void 0,
|
|
2507
|
+
onClick: toggle,
|
|
2508
|
+
children: shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "meter" }) : shown.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2509
|
+
className: "dsm-gp",
|
|
2510
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2511
|
+
className: "dsm-gp-name",
|
|
2512
|
+
children: shortName(snapshot.status, provider)
|
|
2513
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minis, {
|
|
2514
|
+
t,
|
|
2515
|
+
windows: windowsOf(snapshot, provider),
|
|
2516
|
+
display: prefView.display,
|
|
2517
|
+
now
|
|
2518
|
+
})]
|
|
2519
|
+
}, provider))
|
|
2520
|
+
}), open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Popover, {
|
|
2521
|
+
t,
|
|
2522
|
+
store,
|
|
2523
|
+
prefs,
|
|
2524
|
+
snapshot,
|
|
2525
|
+
prefView,
|
|
2526
|
+
shown,
|
|
2527
|
+
now,
|
|
2528
|
+
anchor,
|
|
2529
|
+
onClose: () => {
|
|
2530
|
+
setOpen(false);
|
|
2531
|
+
anchor.current?.focus();
|
|
2532
|
+
},
|
|
2533
|
+
...openSettings === void 0 ? {} : { onOpenSettings: () => {
|
|
2534
|
+
setOpen(false);
|
|
2535
|
+
openSettings();
|
|
2536
|
+
} }
|
|
2537
|
+
}), document.body) : null]
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
function Popover({ t, store, prefs, snapshot, prefView, shown, now, anchor, onClose, onOpenSettings }) {
|
|
2541
|
+
const panel = (0, react.useRef)(null);
|
|
2542
|
+
const [position, setPosition] = (0, react.useState)();
|
|
2543
|
+
const [openSections, setOpenSections] = (0, react.useState)(() => new Set(shown));
|
|
2544
|
+
const [othersOpen, setOthersOpen] = (0, react.useState)(false);
|
|
2545
|
+
const [openMore, setOpenMore] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
2546
|
+
const [manualAt, setManualAt] = (0, react.useState)();
|
|
2547
|
+
(0, react.useLayoutEffect)(() => {
|
|
2548
|
+
const place = () => {
|
|
2549
|
+
const a = anchor.current?.getBoundingClientRect();
|
|
2550
|
+
const p = panel.current?.getBoundingClientRect();
|
|
2551
|
+
if (a === void 0 || p === void 0) return;
|
|
2552
|
+
const left = Math.min(Math.max(8, a.left + a.width / 2 - p.width / 2), window.innerWidth - p.width - 8);
|
|
2553
|
+
const above = a.top - p.height - 8;
|
|
2554
|
+
setPosition({
|
|
2555
|
+
left,
|
|
2556
|
+
top: above >= 8 ? above : a.bottom + 8
|
|
2557
|
+
});
|
|
2558
|
+
};
|
|
2559
|
+
place();
|
|
2560
|
+
window.addEventListener("resize", place);
|
|
2561
|
+
return () => {
|
|
2562
|
+
window.removeEventListener("resize", place);
|
|
2563
|
+
};
|
|
2564
|
+
}, [
|
|
2565
|
+
anchor,
|
|
2566
|
+
openSections,
|
|
2567
|
+
openMore,
|
|
2568
|
+
othersOpen,
|
|
2569
|
+
snapshot
|
|
2570
|
+
]);
|
|
2571
|
+
(0, react.useEffect)(() => {
|
|
2572
|
+
const onKey = (event) => {
|
|
2573
|
+
if (event.key === "Escape") onClose();
|
|
2574
|
+
};
|
|
2575
|
+
const onPointer = (event) => {
|
|
2576
|
+
const target = event.target;
|
|
2577
|
+
if (!(target instanceof Node)) return;
|
|
2578
|
+
if (panel.current?.contains(target) === true || anchor.current?.contains(target) === true) return;
|
|
2579
|
+
onClose();
|
|
2580
|
+
};
|
|
2581
|
+
document.addEventListener("keydown", onKey);
|
|
2582
|
+
document.addEventListener("pointerdown", onPointer, true);
|
|
2583
|
+
return () => {
|
|
2584
|
+
document.removeEventListener("keydown", onKey);
|
|
2585
|
+
document.removeEventListener("pointerdown", onPointer, true);
|
|
2586
|
+
};
|
|
2587
|
+
}, [anchor, onClose]);
|
|
2588
|
+
const { others } = popoverProviders(shown, snapshot);
|
|
2589
|
+
const refreshing = snapshot.refreshing.size > 0;
|
|
2590
|
+
const cooling = manualAt !== void 0 && now - manualAt < 3e4;
|
|
2591
|
+
const toggleIn = (set, id) => {
|
|
2592
|
+
const next = new Set(set);
|
|
2593
|
+
if (next.has(id)) next.delete(id);
|
|
2594
|
+
else next.add(id);
|
|
2595
|
+
return next;
|
|
2596
|
+
};
|
|
2597
|
+
const renderSection = (provider) => {
|
|
2598
|
+
const status = snapshot.status?.providers[provider];
|
|
2599
|
+
if (status === void 0) return null;
|
|
2600
|
+
const split = splitAccounts(status);
|
|
2601
|
+
if (split === void 0) return null;
|
|
2602
|
+
const { active, rest } = split;
|
|
2603
|
+
const sectionOpen = openSections.has(provider);
|
|
2604
|
+
const pinned = prefView.pinned.includes(provider);
|
|
2605
|
+
const pinLabel = pinned ? t("unpin") : t("pin");
|
|
2606
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2607
|
+
className: "dsm-psec",
|
|
2608
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2609
|
+
className: "dsm-psec-head",
|
|
2610
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2611
|
+
type: "button",
|
|
2612
|
+
className: "dsm-fold",
|
|
2613
|
+
"aria-expanded": sectionOpen,
|
|
2614
|
+
onClick: () => {
|
|
2615
|
+
setOpenSections((s) => toggleIn(s, provider));
|
|
2616
|
+
},
|
|
2617
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2618
|
+
className: "dsm-fold-name",
|
|
2619
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2620
|
+
name: "right",
|
|
2621
|
+
size: 12,
|
|
2622
|
+
className: "dsm-chev"
|
|
2623
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2624
|
+
className: "dsm-psec-name",
|
|
2625
|
+
children: shortName(snapshot.status, provider)
|
|
2626
|
+
})]
|
|
2627
|
+
}), sectionOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minis, {
|
|
2628
|
+
t,
|
|
2629
|
+
windows: windowsOf(snapshot, provider),
|
|
2630
|
+
display: prefView.display,
|
|
2631
|
+
now
|
|
2632
|
+
})]
|
|
2633
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2634
|
+
type: "button",
|
|
2635
|
+
className: "dsm-btn dsm-icon dsm-sm dsm-pin",
|
|
2636
|
+
"aria-pressed": pinned,
|
|
2637
|
+
title: pinLabel,
|
|
2638
|
+
"aria-label": pinLabel,
|
|
2639
|
+
onClick: () => {
|
|
2640
|
+
prefs.togglePinned(provider);
|
|
2641
|
+
},
|
|
2642
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2643
|
+
name: pinned ? "pinFilled" : "pin",
|
|
2644
|
+
size: 12
|
|
2645
|
+
})
|
|
2646
|
+
})]
|
|
2647
|
+
}), sectionOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2648
|
+
className: "dsm-fold-body",
|
|
2649
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
2650
|
+
t,
|
|
2651
|
+
provider,
|
|
2652
|
+
account: active,
|
|
2653
|
+
status,
|
|
2654
|
+
snapshot,
|
|
2655
|
+
prefs: prefView,
|
|
2656
|
+
now,
|
|
2657
|
+
compact: true
|
|
2658
|
+
}), rest.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2659
|
+
type: "button",
|
|
2660
|
+
className: "dsm-fold",
|
|
2661
|
+
"aria-expanded": openMore.has(provider),
|
|
2662
|
+
onClick: () => {
|
|
2663
|
+
setOpenMore((s) => toggleIn(s, provider));
|
|
2664
|
+
},
|
|
2665
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2666
|
+
className: "dsm-fold-name",
|
|
2667
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2668
|
+
name: "right",
|
|
2669
|
+
size: 12,
|
|
2670
|
+
className: "dsm-chev"
|
|
2671
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: rest.length === 1 ? rest[0].account ?? rest[0].key : tf(t, "moreAccounts", { count: rest.length }) })]
|
|
2672
|
+
}), rest.length === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinisFor, {
|
|
2673
|
+
t,
|
|
2674
|
+
snapshot,
|
|
2675
|
+
provider,
|
|
2676
|
+
account: rest[0].key,
|
|
2677
|
+
prefs: prefView,
|
|
2678
|
+
now
|
|
2679
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})]
|
|
2680
|
+
}), openMore.has(provider) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2681
|
+
className: "dsm-fold-body",
|
|
2682
|
+
children: rest.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Account, {
|
|
2683
|
+
t,
|
|
2684
|
+
provider,
|
|
2685
|
+
account,
|
|
2686
|
+
status,
|
|
2687
|
+
snapshot,
|
|
2688
|
+
prefs: prefView,
|
|
2689
|
+
now,
|
|
2690
|
+
compact: true
|
|
2691
|
+
}, account.key))
|
|
2692
|
+
}) : null] })]
|
|
2693
|
+
}) : null]
|
|
2694
|
+
}, provider);
|
|
2695
|
+
};
|
|
2696
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2697
|
+
ref: panel,
|
|
2698
|
+
className: "dsm-pop",
|
|
2699
|
+
role: "dialog",
|
|
2700
|
+
"aria-label": t("usageLabel"),
|
|
2701
|
+
style: position === void 0 ? {
|
|
2702
|
+
visibility: "hidden",
|
|
2703
|
+
left: 0,
|
|
2704
|
+
top: 0
|
|
2705
|
+
} : {
|
|
2706
|
+
left: position.left,
|
|
2707
|
+
top: position.top
|
|
2708
|
+
},
|
|
2709
|
+
children: [
|
|
2710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2711
|
+
className: "dsm-pop-head",
|
|
2712
|
+
children: [
|
|
2713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("usage") }),
|
|
2714
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2715
|
+
type: "button",
|
|
2716
|
+
className: `dsm-btn dsm-icon${refreshing ? " dsm-spinning" : ""}`,
|
|
2717
|
+
disabled: refreshing || cooling,
|
|
2718
|
+
title: cooling ? tf(t, "refreshedWait", { seconds: Math.ceil((3e4 - (now - (manualAt ?? now))) / 1e3) }) : t("refreshNow"),
|
|
2719
|
+
"aria-label": t("refreshNow"),
|
|
2720
|
+
onClick: () => {
|
|
2721
|
+
setManualAt(Date.now());
|
|
2722
|
+
store.refreshUsage("manual");
|
|
2723
|
+
},
|
|
2724
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: cooling && !refreshing ? "check" : "refresh" })
|
|
2725
|
+
}),
|
|
2726
|
+
onOpenSettings === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2727
|
+
type: "button",
|
|
2728
|
+
className: "dsm-btn dsm-icon",
|
|
2729
|
+
title: t("usageSettings"),
|
|
2730
|
+
"aria-label": t("usageSettings"),
|
|
2731
|
+
onClick: onOpenSettings,
|
|
2732
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "gear" })
|
|
2733
|
+
})
|
|
2734
|
+
]
|
|
2735
|
+
}),
|
|
2736
|
+
shown.map(renderSection),
|
|
2737
|
+
shown.length === 0 ? others.map(renderSection) : others.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2738
|
+
className: "dsm-psec dsm-others",
|
|
2739
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2740
|
+
type: "button",
|
|
2741
|
+
className: "dsm-fold",
|
|
2742
|
+
"aria-expanded": othersOpen,
|
|
2743
|
+
onClick: () => {
|
|
2744
|
+
setOthersOpen((value) => !value);
|
|
2745
|
+
},
|
|
2746
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2747
|
+
className: "dsm-fold-name",
|
|
2748
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2749
|
+
name: "right",
|
|
2750
|
+
size: 12,
|
|
2751
|
+
className: "dsm-chev"
|
|
2752
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2753
|
+
className: "dsm-psec-name",
|
|
2754
|
+
children: tf(t, "otherProviders", { count: others.length })
|
|
2755
|
+
})]
|
|
2756
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})]
|
|
2757
|
+
}), othersOpen ? others.map(renderSection) : null]
|
|
2758
|
+
})
|
|
2759
|
+
]
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
function MinisFor({ t, snapshot, provider, account, prefs, now }) {
|
|
2763
|
+
const entry = snapshot.usage.get(usageKey(provider, account));
|
|
2764
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minis, {
|
|
2765
|
+
t,
|
|
2766
|
+
windows: entry?.kind === "ready" ? entry.usage.windows : void 0,
|
|
2767
|
+
display: prefs.display,
|
|
2768
|
+
now
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
//#endregion
|
|
2773
|
+
//#region src/client/Notice.tsx
|
|
2774
|
+
const DISMISSED_KEY = "dsh-plugin-subscription-usage.dismissed-notices";
|
|
2775
|
+
/** Dismissals survive reloads; one entry per window crossing. */
|
|
2776
|
+
function readDismissed() {
|
|
2777
|
+
try {
|
|
2778
|
+
const raw = JSON.parse(localStorage.getItem(DISMISSED_KEY) ?? "[]");
|
|
2779
|
+
return new Set(Array.isArray(raw) ? raw.filter((id) => typeof id === "string") : []);
|
|
2780
|
+
} catch {
|
|
2781
|
+
return /* @__PURE__ */ new Set();
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* The most urgent notice across the serving accounts, or undefined.
|
|
2786
|
+
* Only serving or default accounts warn; an idle account's pace is not the user's problem yet.
|
|
2787
|
+
*/
|
|
2788
|
+
function currentNotice(t, snapshot, now, dismissed) {
|
|
2789
|
+
const candidates = [];
|
|
2790
|
+
for (const provider of USAGE_PROVIDERS) {
|
|
2791
|
+
if (!isMetered(snapshot.status, provider)) continue;
|
|
2792
|
+
const status = snapshot.status?.providers[provider];
|
|
2793
|
+
if (status === void 0) continue;
|
|
2794
|
+
for (const account of status.accounts) {
|
|
2795
|
+
if (account.key !== status.servingAccount && !account.isDefault) continue;
|
|
2796
|
+
const entry = snapshot.usage.get(usageKey(provider, account.key));
|
|
2797
|
+
if (entry?.kind !== "ready") continue;
|
|
2798
|
+
for (const window$1 of entry.usage.windows ?? []) {
|
|
2799
|
+
const notice = noticeFor(t, shortName(snapshot.status, provider), provider, account.key, window$1, now);
|
|
2800
|
+
if (notice !== void 0 && !dismissed.has(notice.id)) candidates.push(notice);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
candidates.sort((a, b) => a.rank - b.rank);
|
|
2805
|
+
const top = candidates[0];
|
|
2806
|
+
return top === void 0 ? void 0 : {
|
|
2807
|
+
id: top.id,
|
|
2808
|
+
tone: top.tone,
|
|
2809
|
+
text: top.text
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
function noticeFor(t, name, provider, account, window$1, now) {
|
|
2813
|
+
const p = pace(window$1, now);
|
|
2814
|
+
const id = `${provider}/${account}/${window$1.kind}/${window$1.scope ?? ""}/${window$1.resetsAt ?? ""}`;
|
|
2815
|
+
const label$1 = windowLabel(t, window$1);
|
|
2816
|
+
if (p.state === "out") return {
|
|
2817
|
+
id: `${id}/out`,
|
|
2818
|
+
tone: "error",
|
|
2819
|
+
rank: 0,
|
|
2820
|
+
text: tf(t, "noticeOut", {
|
|
2821
|
+
provider: name,
|
|
2822
|
+
window: label$1
|
|
2823
|
+
})
|
|
2824
|
+
};
|
|
2825
|
+
if (p.state === "over") return {
|
|
2826
|
+
id: `${id}/over`,
|
|
2827
|
+
tone: "warn",
|
|
2828
|
+
rank: p.emptyAt - now,
|
|
2829
|
+
text: tf(t, "noticeEmpty", {
|
|
2830
|
+
provider: name,
|
|
2831
|
+
window: label$1,
|
|
2832
|
+
time: clockTime(p.emptyAt, now)
|
|
2833
|
+
})
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
function Notice(props) {
|
|
2837
|
+
const { store, prefs, t } = props;
|
|
2838
|
+
if (store === void 0 || prefs === void 0 || t === void 0) return null;
|
|
2839
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NoticeBody, {
|
|
2840
|
+
store,
|
|
2841
|
+
prefs,
|
|
2842
|
+
t
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
function NoticeBody({ store, prefs, t }) {
|
|
2846
|
+
const snapshot = useStore(store);
|
|
2847
|
+
const view = usePrefs(prefs);
|
|
2848
|
+
const now = useNow(6e4);
|
|
2849
|
+
const [dismissed, setDismissed] = (0, react.useState)(readDismissed);
|
|
2850
|
+
const notice = (0, react.useMemo)(() => view.paceWarnings ? currentNotice(t, snapshot, now, dismissed) : void 0, [
|
|
2851
|
+
view.paceWarnings,
|
|
2852
|
+
t,
|
|
2853
|
+
snapshot,
|
|
2854
|
+
now,
|
|
2855
|
+
dismissed
|
|
2856
|
+
]);
|
|
2857
|
+
(0, react.useEffect)(() => {
|
|
2858
|
+
const onStorage = (event) => {
|
|
2859
|
+
if (event.key === DISMISSED_KEY) setDismissed(readDismissed());
|
|
2860
|
+
};
|
|
2861
|
+
window.addEventListener("storage", onStorage);
|
|
2862
|
+
return () => {
|
|
2863
|
+
window.removeEventListener("storage", onStorage);
|
|
2864
|
+
};
|
|
2865
|
+
}, []);
|
|
2866
|
+
if (notice === void 0) return null;
|
|
2867
|
+
const dismiss = () => {
|
|
2868
|
+
const next = new Set(dismissed);
|
|
2869
|
+
next.add(notice.id);
|
|
2870
|
+
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...next].slice(-50)));
|
|
2871
|
+
setDismissed(next);
|
|
2872
|
+
};
|
|
2873
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2874
|
+
className: "dsm-notice-host",
|
|
2875
|
+
style: { pointerEvents: "auto" },
|
|
2876
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2877
|
+
className: `dsm-notice${notice.tone === "error" ? " dsm-err" : ""}`,
|
|
2878
|
+
role: "status",
|
|
2879
|
+
"aria-live": "polite",
|
|
2880
|
+
children: [
|
|
2881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "alert" }),
|
|
2882
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: notice.text }),
|
|
2883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2884
|
+
type: "button",
|
|
2885
|
+
className: "dsm-btn dsm-icon",
|
|
2886
|
+
"aria-label": t("dismiss"),
|
|
2887
|
+
title: t("dismiss"),
|
|
2888
|
+
onClick: dismiss,
|
|
2889
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "x" })
|
|
2890
|
+
})
|
|
2891
|
+
]
|
|
2892
|
+
})
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
//#endregion
|
|
2897
|
+
//#region src/client/ImageGallery.tsx
|
|
2898
|
+
/** Display box for a lone image (platform rule): long edge 240px with the
|
|
2899
|
+
* rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
|
|
2900
|
+
* `object-fit: cover` — and never upscaled past the image's natural size. The
|
|
2901
|
+
* crop anchor keeps the top of very tall images and the left of very wide
|
|
2902
|
+
* ones, where the informative content usually starts. */
|
|
2903
|
+
function singleFit(attachment) {
|
|
2904
|
+
const natural = attachment.width / attachment.height;
|
|
2905
|
+
const ratio = Math.min(4, Math.max(.25, natural));
|
|
2906
|
+
const box = ratio >= 1 ? {
|
|
2907
|
+
width: 240,
|
|
2908
|
+
height: 240 / ratio
|
|
2909
|
+
} : {
|
|
2910
|
+
width: 240 * ratio,
|
|
2911
|
+
height: 240
|
|
2912
|
+
};
|
|
2913
|
+
const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height);
|
|
2914
|
+
return {
|
|
2915
|
+
width: Math.max(1, Math.round(box.width * scale)),
|
|
2916
|
+
height: Math.max(1, Math.round(box.height * scale)),
|
|
2917
|
+
objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const styles$3 = {
|
|
2921
|
+
gallery: {
|
|
2922
|
+
display: "flex",
|
|
2923
|
+
flexWrap: "wrap",
|
|
2924
|
+
gap: 8,
|
|
2925
|
+
justifyContent: "flex-start"
|
|
2926
|
+
},
|
|
2927
|
+
frame: {
|
|
2928
|
+
display: "grid",
|
|
2929
|
+
placeItems: "center",
|
|
2930
|
+
overflow: "hidden",
|
|
2931
|
+
padding: 0,
|
|
2932
|
+
border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
|
|
2933
|
+
borderRadius: 8,
|
|
2934
|
+
background: "var(--dsw-alias-interactive-bg-hover-solid)",
|
|
2935
|
+
cursor: "zoom-in"
|
|
2936
|
+
},
|
|
2937
|
+
tile: {
|
|
2938
|
+
width: 64,
|
|
2939
|
+
height: 64
|
|
2940
|
+
},
|
|
2941
|
+
img: {
|
|
2942
|
+
width: "100%",
|
|
2943
|
+
height: "100%",
|
|
2944
|
+
objectFit: "cover",
|
|
2945
|
+
display: "block"
|
|
2946
|
+
},
|
|
2947
|
+
loading: {
|
|
2948
|
+
fontSize: 12,
|
|
2949
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2950
|
+
padding: "0 8px"
|
|
2951
|
+
},
|
|
2952
|
+
error: {
|
|
2953
|
+
fontSize: 12,
|
|
2954
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
2955
|
+
cursor: "pointer",
|
|
2956
|
+
border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
|
|
2957
|
+
borderRadius: 8,
|
|
2958
|
+
background: "transparent",
|
|
2959
|
+
padding: "6px 10px"
|
|
2960
|
+
},
|
|
2961
|
+
overlay: {
|
|
2962
|
+
position: "fixed",
|
|
2963
|
+
inset: 0,
|
|
2964
|
+
zIndex: 1e3,
|
|
2965
|
+
display: "grid",
|
|
2966
|
+
placeItems: "center",
|
|
2967
|
+
background: "rgba(0, 0, 0, 0.72)",
|
|
2968
|
+
padding: 24
|
|
2969
|
+
},
|
|
2970
|
+
overlayImg: {
|
|
2971
|
+
maxWidth: "92vw",
|
|
2972
|
+
maxHeight: "92vh",
|
|
2973
|
+
objectFit: "contain",
|
|
2974
|
+
borderRadius: 4
|
|
2975
|
+
},
|
|
2976
|
+
close: {
|
|
2977
|
+
position: "absolute",
|
|
2978
|
+
top: 12,
|
|
2979
|
+
right: 12,
|
|
2980
|
+
width: 32,
|
|
2981
|
+
height: 32,
|
|
2982
|
+
display: "grid",
|
|
2983
|
+
placeItems: "center",
|
|
2984
|
+
border: "none",
|
|
2985
|
+
borderRadius: "50%",
|
|
2986
|
+
cursor: "pointer",
|
|
2987
|
+
background: "rgba(255, 255, 255, 0.16)",
|
|
2988
|
+
color: "#fff",
|
|
2989
|
+
fontSize: 16,
|
|
2990
|
+
lineHeight: 1
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
/**
|
|
2994
|
+
* Full-viewport original-image preview: backdrop or close-control click and
|
|
2995
|
+
* Escape all dismiss; the image itself is inert so a click on it does not
|
|
2996
|
+
* fall through to the backdrop dismissal.
|
|
2997
|
+
*/
|
|
2998
|
+
function ImageLightbox({ src, alt, labels, onClose }) {
|
|
2999
|
+
(0, react.useEffect)(() => {
|
|
3000
|
+
const onKey = (event) => {
|
|
3001
|
+
if (event.key === "Escape") onClose();
|
|
3002
|
+
};
|
|
3003
|
+
window.addEventListener("keydown", onKey);
|
|
3004
|
+
return () => {
|
|
3005
|
+
window.removeEventListener("keydown", onKey);
|
|
3006
|
+
};
|
|
3007
|
+
}, [onClose]);
|
|
3008
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3009
|
+
role: "dialog",
|
|
3010
|
+
"aria-label": labels.dialog,
|
|
3011
|
+
style: styles$3.overlay,
|
|
3012
|
+
onClick: onClose,
|
|
3013
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3014
|
+
src,
|
|
3015
|
+
alt,
|
|
3016
|
+
style: styles$3.overlayImg,
|
|
3017
|
+
onClick: (event) => {
|
|
3018
|
+
event.stopPropagation();
|
|
3019
|
+
}
|
|
3020
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3021
|
+
type: "button",
|
|
3022
|
+
"aria-label": labels.close,
|
|
3023
|
+
style: styles$3.close,
|
|
3024
|
+
onClick: onClose,
|
|
3025
|
+
children: "×"
|
|
3026
|
+
})]
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Compact history renderer with retryable loading and click-to-open original
|
|
3031
|
+
* preview. A lone image renders at its `singleFit` size; an image among
|
|
3032
|
+
* several renders as a fixed 64px square tile.
|
|
3033
|
+
*/
|
|
3034
|
+
function MessageImage({ attachment, load, variant, labels }) {
|
|
3035
|
+
const [src, setSrc] = (0, react.useState)(null);
|
|
3036
|
+
const [error, setError] = (0, react.useState)(false);
|
|
3037
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
3038
|
+
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
3039
|
+
const retry = (0, react.useCallback)(() => {
|
|
3040
|
+
setAttempt((a) => a + 1);
|
|
3041
|
+
}, []);
|
|
3042
|
+
const close = (0, react.useCallback)(() => {
|
|
3043
|
+
setOpen(false);
|
|
3044
|
+
}, []);
|
|
3045
|
+
const fit = (0, react.useMemo)(() => variant === "single" ? singleFit(attachment) : void 0, [attachment, variant]);
|
|
3046
|
+
(0, react.useEffect)(() => {
|
|
3047
|
+
let live = true;
|
|
3048
|
+
setError(false);
|
|
3049
|
+
setSrc(null);
|
|
3050
|
+
load(attachment).then((url) => {
|
|
3051
|
+
if (live) setSrc(url);
|
|
3052
|
+
}).catch(() => {
|
|
3053
|
+
if (live) setError(true);
|
|
3054
|
+
});
|
|
3055
|
+
return () => {
|
|
3056
|
+
live = false;
|
|
3057
|
+
};
|
|
3058
|
+
}, [
|
|
3059
|
+
attachment,
|
|
3060
|
+
load,
|
|
3061
|
+
attempt
|
|
3062
|
+
]);
|
|
3063
|
+
const label$1 = attachment.name ?? labels.image;
|
|
3064
|
+
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3065
|
+
type: "button",
|
|
3066
|
+
style: styles$3.error,
|
|
3067
|
+
onClick: retry,
|
|
3068
|
+
children: labels.loadFailed
|
|
3069
|
+
});
|
|
3070
|
+
const box = fit === void 0 ? styles$3.tile : {
|
|
3071
|
+
width: fit.width,
|
|
3072
|
+
height: fit.height
|
|
3073
|
+
};
|
|
3074
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3075
|
+
type: "button",
|
|
3076
|
+
style: {
|
|
3077
|
+
...styles$3.frame,
|
|
3078
|
+
...box
|
|
3079
|
+
},
|
|
3080
|
+
title: labels.open,
|
|
3081
|
+
"aria-label": labels.openNamed(label$1),
|
|
3082
|
+
onClick: () => {
|
|
3083
|
+
if (src !== null) setOpen(true);
|
|
3084
|
+
},
|
|
3085
|
+
children: src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3086
|
+
style: styles$3.loading,
|
|
3087
|
+
children: labels.loading
|
|
3088
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3089
|
+
src,
|
|
3090
|
+
alt: label$1,
|
|
3091
|
+
style: {
|
|
3092
|
+
...styles$3.img,
|
|
3093
|
+
objectPosition: fit?.objectPosition
|
|
3094
|
+
}
|
|
3095
|
+
})
|
|
3096
|
+
}), open && src !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
|
|
3097
|
+
src,
|
|
3098
|
+
alt: label$1,
|
|
3099
|
+
labels: labels.lightbox,
|
|
3100
|
+
onClose: close
|
|
3101
|
+
})] });
|
|
3102
|
+
}
|
|
3103
|
+
/** Wrapping image group: a lone image renders large, several render as 64px
|
|
3104
|
+
* square tiles (same rule as the platform gallery). */
|
|
3105
|
+
function ImageGallery({ images, load, labels }) {
|
|
3106
|
+
if (images.length === 0) return null;
|
|
3107
|
+
const variant = images.length === 1 ? "single" : "tile";
|
|
3108
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3109
|
+
style: styles$3.gallery,
|
|
3110
|
+
children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImage, {
|
|
3111
|
+
attachment: image.attachment,
|
|
3112
|
+
load,
|
|
3113
|
+
variant,
|
|
3114
|
+
labels
|
|
3115
|
+
}, `${image.attachment.attachmentId}:${index}`))
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
//#endregion
|
|
3120
|
+
//#region src/client/ImageGenerateToolview.tsx
|
|
3121
|
+
/** Title prompt truncation budget (characters). */
|
|
3122
|
+
const PROMPT_MAX_LENGTH$1 = 60;
|
|
3123
|
+
/**
|
|
3124
|
+
* Build the ImageGallery loader over the `image` endpoint.
|
|
3125
|
+
* @param rpc - Connection RPC caller.
|
|
3126
|
+
* @returns loader resolving an attachment ref to a data URL.
|
|
3127
|
+
*/
|
|
3128
|
+
function createImageLoader(rpc) {
|
|
3129
|
+
return (attachment) => call(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* English-dictionary fallback for a missing locale seat (standalone renders);
|
|
3133
|
+
* the framework always supplies the namespace-bound one.
|
|
3134
|
+
* @param key - dictionary key.
|
|
3135
|
+
* @param params - `{name}` template params.
|
|
3136
|
+
* @returns the template with params substituted.
|
|
3137
|
+
*/
|
|
3138
|
+
function fallbackTranslate$2(key, params) {
|
|
3139
|
+
let text = en[key];
|
|
3140
|
+
for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
|
|
3141
|
+
return text;
|
|
3142
|
+
}
|
|
3143
|
+
/** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
|
|
3144
|
+
function derivePrompt$1(argsRaw) {
|
|
3145
|
+
let parsed;
|
|
3146
|
+
try {
|
|
3147
|
+
parsed = JSON.parse(argsRaw);
|
|
3148
|
+
} catch {
|
|
3149
|
+
parsed = void 0;
|
|
3150
|
+
}
|
|
3151
|
+
let prompt;
|
|
3152
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3153
|
+
const args = parsed;
|
|
3154
|
+
if (typeof args.prompt === "string" && args.prompt !== "") prompt = args.prompt;
|
|
3155
|
+
else for (const value of Object.values(args)) if (typeof value === "string" && value !== "") {
|
|
3156
|
+
prompt = value;
|
|
3157
|
+
break;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
const line = (prompt ?? argsRaw).split("\n", 1)[0] ?? "";
|
|
3161
|
+
return line.length > PROMPT_MAX_LENGTH$1 ? `${line.slice(0, PROMPT_MAX_LENGTH$1)}…` : line;
|
|
3162
|
+
}
|
|
3163
|
+
/** Flatten a settled result's text blocks (the degraded text-only route and the error line). */
|
|
3164
|
+
function resultText$1(block) {
|
|
3165
|
+
if (!("kind" in block)) return "";
|
|
3166
|
+
const parts = [];
|
|
3167
|
+
for (const part of block.content) if (part.type === "text") parts.push(part.text);
|
|
3168
|
+
if (parts.length === 0 && block.error !== void 0) parts.push(`${block.error.name}: ${block.error.code}`);
|
|
3169
|
+
return parts.join("\n");
|
|
3170
|
+
}
|
|
3171
|
+
/** Image attachments of a settled result; empty while running or on the text-only route. */
|
|
3172
|
+
function resultImages(block) {
|
|
3173
|
+
if (!("kind" in block)) return [];
|
|
3174
|
+
const images = [];
|
|
3175
|
+
for (const part of block.content) if (part.type === "image") images.push({ attachment: part.attachment });
|
|
3176
|
+
return images;
|
|
3177
|
+
}
|
|
3178
|
+
const styles$2 = {
|
|
3179
|
+
container: {
|
|
3180
|
+
display: "flex",
|
|
3181
|
+
flexDirection: "column",
|
|
3182
|
+
gap: 6,
|
|
3183
|
+
padding: "4px 0"
|
|
3184
|
+
},
|
|
3185
|
+
row: {
|
|
3186
|
+
display: "flex",
|
|
3187
|
+
alignItems: "center",
|
|
3188
|
+
gap: 6,
|
|
3189
|
+
minWidth: 0
|
|
3190
|
+
},
|
|
3191
|
+
icon: {
|
|
3192
|
+
display: "inline-flex",
|
|
3193
|
+
flexShrink: 0,
|
|
3194
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3195
|
+
},
|
|
3196
|
+
title: {
|
|
3197
|
+
fontSize: 13,
|
|
3198
|
+
lineHeight: "20px",
|
|
3199
|
+
color: "var(--dsw-alias-label-primary)",
|
|
3200
|
+
overflow: "hidden",
|
|
3201
|
+
textOverflow: "ellipsis",
|
|
3202
|
+
whiteSpace: "nowrap"
|
|
3203
|
+
},
|
|
3204
|
+
subtle: {
|
|
3205
|
+
margin: 0,
|
|
3206
|
+
fontSize: 12,
|
|
3207
|
+
lineHeight: "18px",
|
|
3208
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3209
|
+
},
|
|
3210
|
+
output: {
|
|
3211
|
+
margin: 0,
|
|
3212
|
+
fontSize: 12,
|
|
3213
|
+
lineHeight: "18px",
|
|
3214
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
3215
|
+
whiteSpace: "pre-wrap",
|
|
3216
|
+
overflowWrap: "anywhere"
|
|
3217
|
+
},
|
|
3218
|
+
error: {
|
|
3219
|
+
margin: 0,
|
|
3220
|
+
fontSize: 12,
|
|
3221
|
+
lineHeight: "18px",
|
|
3222
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3225
|
+
/**
|
|
3226
|
+
* The `image_generate` keyed toolview component.
|
|
3227
|
+
* @param props - owner share, inject face, and locale seat (spread flat).
|
|
3228
|
+
* @returns the call row plus, once settled, the gallery / text / error body.
|
|
3229
|
+
*/
|
|
3230
|
+
function ImageGenerateToolview(props) {
|
|
3231
|
+
const { block, load } = props;
|
|
3232
|
+
const t = props.t ?? fallbackTranslate$2;
|
|
3233
|
+
if (block === void 0) return null;
|
|
3234
|
+
const settled = "kind" in block;
|
|
3235
|
+
const argsRaw = (settled ? block.call?.argsRaw : "argsRaw" in block ? block.argsRaw : void 0) ?? "";
|
|
3236
|
+
let references = 0;
|
|
3237
|
+
try {
|
|
3238
|
+
const args = JSON.parse(argsRaw);
|
|
3239
|
+
if (Array.isArray(args?.referenceImages)) references = args.referenceImages.length;
|
|
3240
|
+
} catch {}
|
|
3241
|
+
const title = `image_generate${references > 0 ? ` (${references} ref)` : ""}: ${derivePrompt$1(argsRaw)}`;
|
|
3242
|
+
const images = resultImages(block);
|
|
3243
|
+
const text = settled ? resultText$1(block) : "";
|
|
3244
|
+
const labels = {
|
|
3245
|
+
image: t("image"),
|
|
3246
|
+
open: t("viewImage"),
|
|
3247
|
+
openNamed: (name) => t("viewImageNamed", { name }),
|
|
3248
|
+
loading: t("imageLoading"),
|
|
3249
|
+
loadFailed: t("imageLoadFailed"),
|
|
3250
|
+
lightbox: {
|
|
3251
|
+
dialog: t("imagePreview"),
|
|
3252
|
+
close: t("imageClose")
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3256
|
+
style: styles$2.container,
|
|
3257
|
+
children: [
|
|
3258
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3259
|
+
style: styles$2.row,
|
|
3260
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3261
|
+
style: styles$2.icon,
|
|
3262
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
3263
|
+
name: "sparkle",
|
|
3264
|
+
size: 14
|
|
3265
|
+
})
|
|
3266
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3267
|
+
style: styles$2.title,
|
|
3268
|
+
children: title
|
|
3269
|
+
})]
|
|
3270
|
+
}),
|
|
3271
|
+
!settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3272
|
+
style: styles$2.subtle,
|
|
3273
|
+
children: t("generating")
|
|
3274
|
+
}),
|
|
3275
|
+
settled && block.isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3276
|
+
style: styles$2.error,
|
|
3277
|
+
children: text.split("\n", 1)[0]
|
|
3278
|
+
}),
|
|
3279
|
+
settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGallery, {
|
|
3280
|
+
images,
|
|
3281
|
+
load,
|
|
3282
|
+
labels
|
|
3283
|
+
}),
|
|
3284
|
+
settled && !block.isError && images.length === 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3285
|
+
style: styles$2.output,
|
|
3286
|
+
children: text
|
|
3287
|
+
})
|
|
3288
|
+
]
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
//#endregion
|
|
3293
|
+
//#region src/client/VideoGenerateToolview.tsx
|
|
3294
|
+
/** Title prompt truncation budget (characters). */
|
|
3295
|
+
const PROMPT_MAX_LENGTH = 60;
|
|
3296
|
+
/**
|
|
3297
|
+
* Build the video loader over the `/subscriptions-auth` `video` endpoint.
|
|
3298
|
+
* @param rpc - Connection RPC caller.
|
|
3299
|
+
* @returns loader resolving a bare file name to the decoded bytes.
|
|
3300
|
+
*/
|
|
3301
|
+
function createVideoLoader(rpc) {
|
|
3302
|
+
return (name) => call(rpc, "video", { name });
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* English-dictionary fallback for a missing locale seat (standalone renders);
|
|
3306
|
+
* the framework always supplies the namespace-bound one.
|
|
3307
|
+
*/
|
|
3308
|
+
function fallbackTranslate$1(key, params) {
|
|
3309
|
+
let text = en[key];
|
|
3310
|
+
for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
|
|
3311
|
+
return text;
|
|
3312
|
+
}
|
|
3313
|
+
/** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
|
|
3314
|
+
function derivePrompt(argsRaw) {
|
|
3315
|
+
let parsed;
|
|
3316
|
+
try {
|
|
3317
|
+
parsed = JSON.parse(argsRaw);
|
|
3318
|
+
} catch {
|
|
3319
|
+
parsed = void 0;
|
|
3320
|
+
}
|
|
3321
|
+
let prompt;
|
|
3322
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3323
|
+
const args = parsed;
|
|
3324
|
+
if (typeof args.prompt === "string" && args.prompt !== "") prompt = args.prompt;
|
|
3325
|
+
else for (const value of Object.values(args)) if (typeof value === "string" && value !== "") {
|
|
3326
|
+
prompt = value;
|
|
3327
|
+
break;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
const line = (prompt ?? argsRaw).split("\n", 1)[0] ?? "";
|
|
3331
|
+
return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
|
|
3332
|
+
}
|
|
3333
|
+
/** Flatten a settled result's text blocks (the fallback body and the error line). */
|
|
3334
|
+
function resultText(block) {
|
|
3335
|
+
if (!("kind" in block)) return "";
|
|
3336
|
+
const parts = [];
|
|
3337
|
+
for (const part of block.content) if (part.type === "text") parts.push(part.text);
|
|
3338
|
+
if (parts.length === 0 && block.error !== void 0) parts.push(`${block.error.name}: ${block.error.code}`);
|
|
3339
|
+
return parts.join("\n");
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* The generated video's bare file name: presentation meta first (top-level
|
|
3343
|
+
* dispatches), then the render text's "Saved video to …" line (nested
|
|
3344
|
+
* dispatches compute no meta).
|
|
3345
|
+
*/
|
|
3346
|
+
function resolveFileName(block) {
|
|
3347
|
+
if (!("kind" in block)) return void 0;
|
|
3348
|
+
const meta = block.meta;
|
|
3349
|
+
if (typeof meta === "object" && meta !== null) {
|
|
3350
|
+
const fileName = meta.fileName;
|
|
3351
|
+
if (typeof fileName === "string" && fileName.length > 0) return fileName;
|
|
3352
|
+
}
|
|
3353
|
+
const match = /^Saved video to (.+\.mp4)/m.exec(resultText(block));
|
|
3354
|
+
if (match === null) return void 0;
|
|
3355
|
+
const path = match[1];
|
|
3356
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
3357
|
+
}
|
|
3358
|
+
/** Decode a base64 payload into bytes (browser-side; no Buffer). */
|
|
3359
|
+
function base64Bytes(dataBase64) {
|
|
3360
|
+
const binary = atob(dataBase64);
|
|
3361
|
+
const bytes = new Uint8Array(binary.length);
|
|
3362
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
3363
|
+
return bytes;
|
|
3364
|
+
}
|
|
3365
|
+
const styles$1 = {
|
|
3366
|
+
container: {
|
|
3367
|
+
display: "flex",
|
|
3368
|
+
flexDirection: "column",
|
|
3369
|
+
gap: 6,
|
|
3370
|
+
padding: "4px 0"
|
|
3371
|
+
},
|
|
3372
|
+
row: {
|
|
3373
|
+
display: "flex",
|
|
3374
|
+
alignItems: "center",
|
|
3375
|
+
gap: 6,
|
|
3376
|
+
minWidth: 0
|
|
3377
|
+
},
|
|
3378
|
+
icon: {
|
|
3379
|
+
display: "inline-flex",
|
|
3380
|
+
flexShrink: 0,
|
|
3381
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3382
|
+
},
|
|
3383
|
+
title: {
|
|
3384
|
+
fontSize: 13,
|
|
3385
|
+
lineHeight: "20px",
|
|
3386
|
+
color: "var(--dsw-alias-label-primary)",
|
|
3387
|
+
overflow: "hidden",
|
|
3388
|
+
textOverflow: "ellipsis",
|
|
3389
|
+
whiteSpace: "nowrap"
|
|
3390
|
+
},
|
|
3391
|
+
subtle: {
|
|
3392
|
+
margin: 0,
|
|
3393
|
+
fontSize: 12,
|
|
3394
|
+
lineHeight: "18px",
|
|
3395
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3396
|
+
},
|
|
3397
|
+
output: {
|
|
3398
|
+
margin: 0,
|
|
3399
|
+
fontSize: 12,
|
|
3400
|
+
lineHeight: "18px",
|
|
3401
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
3402
|
+
whiteSpace: "pre-wrap",
|
|
3403
|
+
overflowWrap: "anywhere"
|
|
3404
|
+
},
|
|
3405
|
+
error: {
|
|
3406
|
+
margin: 0,
|
|
3407
|
+
fontSize: 12,
|
|
3408
|
+
lineHeight: "18px",
|
|
3409
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
3410
|
+
},
|
|
3411
|
+
video: {
|
|
3412
|
+
display: "block",
|
|
3413
|
+
maxWidth: 480,
|
|
3414
|
+
width: "100%",
|
|
3415
|
+
borderRadius: 8,
|
|
3416
|
+
backgroundColor: "var(--dsw-alias-fill-tertiary)"
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
/**
|
|
3420
|
+
* The `video_generate` keyed toolview component.
|
|
3421
|
+
* @param props - owner share, inject face, and locale seat (spread flat).
|
|
3422
|
+
* @returns the call row plus, once settled, the player / text / error body.
|
|
3423
|
+
*/
|
|
3424
|
+
function VideoGenerateToolview(props) {
|
|
3425
|
+
const { block, loadVideo } = props;
|
|
3426
|
+
const t = props.t ?? fallbackTranslate$1;
|
|
3427
|
+
const settled = block !== void 0 && "kind" in block;
|
|
3428
|
+
const isError = settled && block.isError;
|
|
3429
|
+
const fileName = block !== void 0 && settled && !isError ? resolveFileName(block) : void 0;
|
|
3430
|
+
const [load, setLoad] = (0, react.useState)({ phase: "loading" });
|
|
3431
|
+
(0, react.useEffect)(() => {
|
|
3432
|
+
if (fileName === void 0 || loadVideo === void 0) return;
|
|
3433
|
+
let cancelled = false;
|
|
3434
|
+
let objectUrl;
|
|
3435
|
+
setLoad({ phase: "loading" });
|
|
3436
|
+
loadVideo(fileName).then((video) => {
|
|
3437
|
+
if (cancelled) return;
|
|
3438
|
+
objectUrl = URL.createObjectURL(new Blob([base64Bytes(video.dataBase64).slice()], { type: video.mediaType }));
|
|
3439
|
+
setLoad({
|
|
3440
|
+
phase: "ready",
|
|
3441
|
+
url: objectUrl
|
|
3442
|
+
});
|
|
3443
|
+
}, (error) => {
|
|
3444
|
+
if (cancelled) return;
|
|
3445
|
+
setLoad({
|
|
3446
|
+
phase: "failed",
|
|
3447
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3448
|
+
});
|
|
3449
|
+
});
|
|
3450
|
+
return () => {
|
|
3451
|
+
cancelled = true;
|
|
3452
|
+
if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
|
|
3453
|
+
};
|
|
3454
|
+
}, [fileName, loadVideo]);
|
|
3455
|
+
if (block === void 0) return null;
|
|
3456
|
+
const title = `video_generate: ${derivePrompt((settled ? block.call?.argsRaw : "argsRaw" in block ? block.argsRaw : void 0) ?? "")}`;
|
|
3457
|
+
const text = settled ? resultText(block) : "";
|
|
3458
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3459
|
+
style: styles$1.container,
|
|
3460
|
+
children: [
|
|
3461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3462
|
+
style: styles$1.row,
|
|
3463
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3464
|
+
style: styles$1.icon,
|
|
3465
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
3466
|
+
name: "sparkle",
|
|
3467
|
+
size: 14
|
|
3468
|
+
})
|
|
3469
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3470
|
+
style: styles$1.title,
|
|
3471
|
+
children: title
|
|
3472
|
+
})]
|
|
3473
|
+
}),
|
|
3474
|
+
!settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3475
|
+
style: styles$1.subtle,
|
|
3476
|
+
children: t("generatingVideo")
|
|
3477
|
+
}),
|
|
3478
|
+
settled && isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3479
|
+
style: styles$1.error,
|
|
3480
|
+
children: text.split("\n", 1)[0]
|
|
3481
|
+
}),
|
|
3482
|
+
settled && !isError && fileName !== void 0 && load.phase === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3483
|
+
style: styles$1.subtle,
|
|
3484
|
+
children: t("videoLoading")
|
|
3485
|
+
}),
|
|
3486
|
+
settled && !isError && fileName !== void 0 && load.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3487
|
+
style: styles$1.error,
|
|
3488
|
+
children: t("videoLoadFailed", { message: load.message })
|
|
3489
|
+
}),
|
|
3490
|
+
settled && !isError && fileName !== void 0 && load.phase === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
|
|
3491
|
+
style: styles$1.video,
|
|
3492
|
+
src: load.url,
|
|
3493
|
+
controls: true,
|
|
3494
|
+
preload: "metadata"
|
|
3495
|
+
}),
|
|
3496
|
+
settled && !isError && fileName === void 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3497
|
+
style: styles$1.output,
|
|
3498
|
+
children: text
|
|
3499
|
+
})
|
|
3500
|
+
]
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
//#endregion
|
|
3505
|
+
//#region src/client/SpeedSelect.tsx
|
|
3506
|
+
/**
|
|
3507
|
+
* The `loadSpeed` half of the inject face: the plugin's own speed state plus
|
|
3508
|
+
* the host's current model selection (the visibility gate). A model-RPC
|
|
3509
|
+
* failure throws rather than answering "hidden" — the caller keeps its last
|
|
3510
|
+
* known state, so a transient failure never locks the toggle away.
|
|
3511
|
+
*
|
|
3512
|
+
* `sessionId` is a plain string: slot and command contexts brand it through
|
|
3513
|
+
* different dsh-session copies, and only the service boundary needs one.
|
|
3514
|
+
*
|
|
3515
|
+
* `models` resolves lazily per call: the ui-model-selection service may
|
|
3516
|
+
* register after this plugin applies, and a shell without it (no model seat
|
|
3517
|
+
* at all) simply keeps the toggle hidden.
|
|
3518
|
+
*/
|
|
3519
|
+
function createSpeedLoader(connection, models, sessionId) {
|
|
3520
|
+
return async () => {
|
|
3521
|
+
const directory = models()?.directoryFor(sessionId);
|
|
3522
|
+
const current = (await directory?.load())?.current ?? null;
|
|
3523
|
+
const state = await call(connection.rpc, "speed", {
|
|
3524
|
+
sessionId,
|
|
3525
|
+
...current?.provider === "codex" ? { model: current.model } : {}
|
|
3526
|
+
});
|
|
3527
|
+
const visible = current !== null && current.provider === "codex" && state.fastModels.includes(current.model);
|
|
3528
|
+
const latest = (await directory?.load())?.current ?? null;
|
|
3529
|
+
if (latest?.provider !== current?.provider || latest?.model !== current?.model) throw new Error("Model changed while loading speed");
|
|
3530
|
+
return {
|
|
3531
|
+
visible,
|
|
3532
|
+
tier: state.tier,
|
|
3533
|
+
...visible && current ? { model: current.model } : {}
|
|
3534
|
+
};
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
/** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
|
|
3538
|
+
function createSpeedSetter(connection, sessionId, models) {
|
|
3539
|
+
return async (tier, displayedModel) => {
|
|
3540
|
+
try {
|
|
3541
|
+
const directories = models?.();
|
|
3542
|
+
const current = directories === void 0 ? null : (await directories.directoryFor(sessionId).load()).current;
|
|
3543
|
+
if (models !== void 0 && current?.provider !== "codex") return false;
|
|
3544
|
+
if (displayedModel !== void 0 && current?.model !== displayedModel) return false;
|
|
3545
|
+
await call(connection.rpc, "setSpeed", {
|
|
3546
|
+
sessionId,
|
|
3547
|
+
tier,
|
|
3548
|
+
...current?.provider === "codex" ? { model: current.model } : {}
|
|
3549
|
+
});
|
|
3550
|
+
return true;
|
|
3551
|
+
} catch {
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
/** English-dictionary fallback for a missing inject `t` (standalone renders). */
|
|
3557
|
+
function fallbackTranslate(key) {
|
|
3558
|
+
return en[key];
|
|
3559
|
+
}
|
|
3560
|
+
const TIERS = ["standard", "fast"];
|
|
3561
|
+
/**
|
|
3562
|
+
* The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
|
|
3563
|
+
* that opens a two-row menu (standard/fast with descriptions, check mark on
|
|
3564
|
+
* the current tier). Mount and every open reload the host state so a model
|
|
3565
|
+
* switch made since the last open self-corrects.
|
|
3566
|
+
*/
|
|
3567
|
+
/** How often the control re-reads the host state (model switches arrive only by asking). */
|
|
3568
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
3569
|
+
/**
|
|
3570
|
+
* The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
|
|
3571
|
+
* that opens a two-row menu (standard/fast with descriptions, check mark on
|
|
3572
|
+
* the current tier). The host pushes nothing on a model switch, so the
|
|
3573
|
+
* control re-reads on a slow poll with a single-flight guard; a failed read
|
|
3574
|
+
* keeps the last known state, so a transient RPC failure can never lock the
|
|
3575
|
+
* toggle away (the earlier mount-only load had no recovery path).
|
|
3576
|
+
*/
|
|
3577
|
+
function SpeedSelect({ loadSpeed, setSpeed, t }) {
|
|
3578
|
+
const translate = t ?? fallbackTranslate;
|
|
3579
|
+
const [state, setState] = (0, react.useState)(null);
|
|
3580
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
3581
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3582
|
+
const rootRef = (0, react.useRef)(null);
|
|
3583
|
+
const loadRef = (0, react.useRef)(loadSpeed);
|
|
3584
|
+
loadRef.current = loadSpeed;
|
|
3585
|
+
const generation = (0, react.useRef)(0);
|
|
3586
|
+
const mounted = (0, react.useRef)(false);
|
|
3587
|
+
const refresh = async () => {
|
|
3588
|
+
if (!mounted.current) return;
|
|
3589
|
+
const id = ++generation.current;
|
|
3590
|
+
try {
|
|
3591
|
+
const loaded = await loadRef.current?.();
|
|
3592
|
+
if (loaded && id === generation.current) setState(loaded);
|
|
3593
|
+
} catch {}
|
|
3594
|
+
};
|
|
3595
|
+
(0, react.useEffect)(() => {
|
|
3596
|
+
if (loadRef.current === void 0) return;
|
|
3597
|
+
mounted.current = true;
|
|
3598
|
+
let cancelled = false;
|
|
3599
|
+
let inflight = false;
|
|
3600
|
+
const reload = () => {
|
|
3601
|
+
if (loadRef.current === void 0 || inflight || cancelled) return;
|
|
3602
|
+
inflight = true;
|
|
3603
|
+
refresh().finally(() => {
|
|
3604
|
+
inflight = false;
|
|
3605
|
+
});
|
|
3606
|
+
};
|
|
3607
|
+
reload();
|
|
3608
|
+
const timer = setInterval(reload, POLL_INTERVAL_MS);
|
|
3609
|
+
return () => {
|
|
3610
|
+
mounted.current = false;
|
|
3611
|
+
++generation.current;
|
|
3612
|
+
cancelled = true;
|
|
3613
|
+
clearInterval(timer);
|
|
3614
|
+
};
|
|
3615
|
+
}, []);
|
|
3616
|
+
(0, react.useEffect)(() => {
|
|
3617
|
+
if (!open) return;
|
|
3618
|
+
const closeOutside = (event) => {
|
|
3619
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
3620
|
+
};
|
|
3621
|
+
document.addEventListener("mousedown", closeOutside);
|
|
3622
|
+
return () => {
|
|
3623
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
3624
|
+
};
|
|
3625
|
+
}, [open]);
|
|
3626
|
+
if (loadSpeed === void 0 || setSpeed === void 0 || state === null || !state.visible) return null;
|
|
3627
|
+
const choose = (tier) => {
|
|
3628
|
+
if (busy) return;
|
|
3629
|
+
if (tier === state.tier) {
|
|
3630
|
+
setOpen(false);
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
setBusy(true);
|
|
3634
|
+
setSpeed(tier, state.model).then((ok) => {
|
|
3635
|
+
if (!mounted.current) return;
|
|
3636
|
+
setBusy(false);
|
|
3637
|
+
if (ok) setOpen(false);
|
|
3638
|
+
refresh();
|
|
3639
|
+
}, () => {
|
|
3640
|
+
if (mounted.current) {
|
|
3641
|
+
setBusy(false);
|
|
3642
|
+
refresh();
|
|
3643
|
+
}
|
|
3644
|
+
});
|
|
3645
|
+
};
|
|
3646
|
+
const show = () => {
|
|
3647
|
+
setOpen(true);
|
|
3648
|
+
refresh();
|
|
3649
|
+
};
|
|
3650
|
+
const tierName = (tier) => translate(tier === "fast" ? "speedFast" : "speedStandard");
|
|
3651
|
+
const tierDescription = (tier) => translate(tier === "fast" ? "speedFastDescription" : "speedStandardDescription");
|
|
3652
|
+
const triggerLabel = `${translate("speed")} · ${tierName(state.tier)}`;
|
|
3653
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3654
|
+
ref: rootRef,
|
|
3655
|
+
className: "dsh-subscription-speed",
|
|
3656
|
+
style: styles.root,
|
|
3657
|
+
onKeyDown: (event) => {
|
|
3658
|
+
if (event.key === "Escape" && open) {
|
|
3659
|
+
event.preventDefault();
|
|
3660
|
+
setOpen(false);
|
|
3661
|
+
}
|
|
3662
|
+
},
|
|
3663
|
+
children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3664
|
+
style: styles.menu,
|
|
3665
|
+
role: "menu",
|
|
3666
|
+
"aria-label": translate("speed"),
|
|
3667
|
+
children: TIERS.map((tier) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3668
|
+
type: "button",
|
|
3669
|
+
role: "menuitemradio",
|
|
3670
|
+
"aria-checked": tier === state.tier,
|
|
3671
|
+
style: styles.item,
|
|
3672
|
+
disabled: busy,
|
|
3673
|
+
onClick: () => {
|
|
3674
|
+
choose(tier);
|
|
3675
|
+
},
|
|
3676
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3677
|
+
style: styles.itemCheck,
|
|
3678
|
+
children: tier === state.tier ? "✓" : ""
|
|
3679
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3680
|
+
style: styles.itemText,
|
|
3681
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3682
|
+
style: styles.itemName,
|
|
3683
|
+
children: tierName(tier)
|
|
3684
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3685
|
+
style: styles.itemDescription,
|
|
3686
|
+
children: tierDescription(tier)
|
|
3687
|
+
})]
|
|
3688
|
+
})]
|
|
3689
|
+
}, tier))
|
|
3690
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3691
|
+
type: "button",
|
|
3692
|
+
style: styles.trigger,
|
|
3693
|
+
"aria-haspopup": "menu",
|
|
3694
|
+
"aria-expanded": open,
|
|
3695
|
+
title: triggerLabel,
|
|
3696
|
+
disabled: busy,
|
|
3697
|
+
onClick: () => {
|
|
3698
|
+
if (open) setOpen(false);
|
|
3699
|
+
else show();
|
|
3700
|
+
},
|
|
3701
|
+
children: triggerLabel
|
|
3702
|
+
})]
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
const styles = {
|
|
3706
|
+
root: {
|
|
3707
|
+
position: "relative",
|
|
3708
|
+
display: "inline-flex"
|
|
3709
|
+
},
|
|
3710
|
+
trigger: {
|
|
3711
|
+
border: "0.5px solid var(--dsw-alias-border-l3)",
|
|
3712
|
+
borderRadius: "var(--dsw-radius-sm, 8px)",
|
|
3713
|
+
background: "var(--dsh-subscription-speed-hover, transparent)",
|
|
3714
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
3715
|
+
font: "inherit",
|
|
3716
|
+
fontSize: 12,
|
|
3717
|
+
lineHeight: "18px",
|
|
3718
|
+
padding: "2px 8px",
|
|
3719
|
+
cursor: "pointer",
|
|
3720
|
+
whiteSpace: "nowrap"
|
|
3721
|
+
},
|
|
3722
|
+
menu: {
|
|
3723
|
+
position: "absolute",
|
|
3724
|
+
bottom: "100%",
|
|
3725
|
+
right: 0,
|
|
3726
|
+
marginBottom: 4,
|
|
3727
|
+
minWidth: 180,
|
|
3728
|
+
padding: 4,
|
|
3729
|
+
zIndex: 20,
|
|
3730
|
+
background: "var(--dsw-specific-menu, var(--dsw-alias-bg-layer-2))",
|
|
3731
|
+
border: 0,
|
|
3732
|
+
boxShadow: "var(--dsw-elevation-prominent)",
|
|
3733
|
+
borderRadius: "var(--dsw-radius-lg, 16px)",
|
|
3734
|
+
display: "flex",
|
|
3735
|
+
flexDirection: "column",
|
|
3736
|
+
gap: 2
|
|
3737
|
+
},
|
|
3738
|
+
item: {
|
|
3739
|
+
display: "flex",
|
|
3740
|
+
alignItems: "flex-start",
|
|
3741
|
+
gap: 6,
|
|
3742
|
+
width: "100%",
|
|
3743
|
+
border: "none",
|
|
3744
|
+
borderRadius: "var(--dsw-radius-sm, 8px)",
|
|
3745
|
+
background: "var(--dsh-subscription-speed-hover, transparent)",
|
|
3746
|
+
padding: "6px 8px",
|
|
3747
|
+
cursor: "pointer",
|
|
3748
|
+
font: "inherit",
|
|
3749
|
+
textAlign: "left"
|
|
3750
|
+
},
|
|
3751
|
+
itemCheck: {
|
|
3752
|
+
width: 14,
|
|
3753
|
+
flexShrink: 0,
|
|
3754
|
+
fontSize: 12,
|
|
3755
|
+
lineHeight: "18px",
|
|
3756
|
+
color: "var(--dsw-alias-label-primary)"
|
|
3757
|
+
},
|
|
3758
|
+
itemText: {
|
|
3759
|
+
display: "flex",
|
|
3760
|
+
flexDirection: "column"
|
|
3761
|
+
},
|
|
3762
|
+
itemName: {
|
|
3763
|
+
fontSize: 12,
|
|
3764
|
+
lineHeight: "18px",
|
|
3765
|
+
color: "var(--dsw-alias-label-primary)"
|
|
3766
|
+
},
|
|
3767
|
+
itemDescription: {
|
|
3768
|
+
fontSize: 11,
|
|
3769
|
+
lineHeight: "16px",
|
|
3770
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
|
|
3774
|
+
//#endregion
|
|
3775
|
+
//#region src/client/fast-command.ts
|
|
3776
|
+
/** Build the locale resolver used by the /fast command contribution. */
|
|
3777
|
+
function fastCommandDescription(t) {
|
|
3778
|
+
return () => t();
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
//#endregion
|
|
3782
|
+
//#region src/client/index.ts
|
|
3783
|
+
/** The Host entry id from cordis.patch.yml; the Models page dispatches our card by it. */
|
|
3784
|
+
const ENTRY_ID = "subscription-models";
|
|
3785
|
+
const inject = [
|
|
3786
|
+
"slots",
|
|
3787
|
+
"connection",
|
|
3788
|
+
"locale"
|
|
3789
|
+
];
|
|
3790
|
+
const hasMethod = (value, name) => typeof value === "object" && value !== null && name in value && typeof value[name] === "function";
|
|
3791
|
+
function apply(ctx) {
|
|
3792
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
3793
|
+
en,
|
|
3794
|
+
zh
|
|
3795
|
+
}), "dsh-plugin-subscription-usage: copy");
|
|
3796
|
+
ctx.effect(() => {
|
|
3797
|
+
const style = document.createElement("style");
|
|
3798
|
+
style.dataset.plugin = "dsh-plugin-subscription-usage";
|
|
3799
|
+
style.textContent = css;
|
|
3800
|
+
document.head.appendChild(style);
|
|
3801
|
+
return () => {
|
|
3802
|
+
style.remove();
|
|
3803
|
+
};
|
|
3804
|
+
}, "dsh-plugin-subscription-usage: stylesheet");
|
|
3805
|
+
const connection = ctx.get("connection");
|
|
3806
|
+
const t = ctx.locale.bind(LOCALE_NS);
|
|
3807
|
+
const store = new SubscriptionsStore(connection.rpc);
|
|
3808
|
+
const prefs = new PrefsStore();
|
|
3809
|
+
const surface = () => ({
|
|
3810
|
+
store,
|
|
3811
|
+
prefs,
|
|
3812
|
+
rpc: connection.rpc,
|
|
3813
|
+
t
|
|
3814
|
+
});
|
|
3815
|
+
ctx.inject(["configForms"], (scope) => {
|
|
3816
|
+
const forms = scope.get("configForms");
|
|
3817
|
+
if (!hasMethod(forms, "get")) return;
|
|
3818
|
+
scope.effect(() => prefs.bind(forms.get(ENTRY_ID)), "dsh-plugin-subscription-usage: usage preferences");
|
|
3819
|
+
});
|
|
3820
|
+
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
|
|
3821
|
+
name: "settings.models.provider-card",
|
|
3822
|
+
key: ENTRY_ID,
|
|
3823
|
+
inject: surface
|
|
3824
|
+
}, ProviderCard));
|
|
3825
|
+
ctx.slots.inject("settings.models.footer", () => ctx.slots.register({
|
|
3826
|
+
name: "settings.models.footer",
|
|
3827
|
+
id: "subscription-models",
|
|
3828
|
+
order: 10,
|
|
3829
|
+
inject: surface
|
|
3830
|
+
}, Footer));
|
|
3831
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3832
|
+
name: "settings.section",
|
|
3833
|
+
id: "subscription-usage",
|
|
3834
|
+
order: 11,
|
|
3835
|
+
label: () => t("usage"),
|
|
3836
|
+
inject: surface
|
|
3837
|
+
}, Usage));
|
|
3838
|
+
ctx.slots.inject("settings.section.icon", () => ctx.slots.register({
|
|
3839
|
+
name: "settings.section.icon",
|
|
3840
|
+
id: "subscription-usage"
|
|
3841
|
+
}, UsageNavIcon));
|
|
3842
|
+
const models = () => {
|
|
3843
|
+
const service = ctx.get("modelDirectories");
|
|
3844
|
+
return hasMethod(service, "directoryFor") ? service : void 0;
|
|
3845
|
+
};
|
|
3846
|
+
const gaugeInject = (sessionId) => {
|
|
3847
|
+
let currentModel;
|
|
3848
|
+
try {
|
|
3849
|
+
currentModel = models()?.directoryFor(sessionId).store;
|
|
3850
|
+
} catch {}
|
|
3851
|
+
return {
|
|
3852
|
+
...surface(),
|
|
3853
|
+
sessionId,
|
|
3854
|
+
currentModel
|
|
3855
|
+
};
|
|
3856
|
+
};
|
|
3857
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
3858
|
+
name: "conversation.composer.dock",
|
|
3859
|
+
id: "subscription-models.gauge",
|
|
3860
|
+
order: 10,
|
|
3861
|
+
inject: gaugeInject
|
|
3862
|
+
}, Gauge));
|
|
3863
|
+
ctx.slots.inject("conversation.hero.options", () => ctx.slots.register({
|
|
3864
|
+
name: "conversation.hero.options",
|
|
3865
|
+
id: "subscription-models.gauge",
|
|
3866
|
+
order: 10,
|
|
3867
|
+
inject: gaugeInject
|
|
3868
|
+
}, Gauge));
|
|
3869
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
3870
|
+
name: "shell.overlay",
|
|
3871
|
+
id: "subscription-models.notice",
|
|
3872
|
+
order: 50,
|
|
3873
|
+
inject: surface
|
|
3874
|
+
}, Notice));
|
|
3875
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3876
|
+
name: "tool.call.toolview",
|
|
3877
|
+
key: "image_generate",
|
|
3878
|
+
locale: LOCALE_NS,
|
|
3879
|
+
inject: () => ({ load: createImageLoader(connection.rpc) })
|
|
3880
|
+
}, ImageGenerateToolview));
|
|
3881
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3882
|
+
name: "tool.call.toolview",
|
|
3883
|
+
key: "video_generate",
|
|
3884
|
+
locale: LOCALE_NS,
|
|
3885
|
+
inject: () => ({ loadVideo: createVideoLoader(connection.rpc) })
|
|
3886
|
+
}, VideoGenerateToolview));
|
|
3887
|
+
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
3888
|
+
name: "conversation.input.right",
|
|
3889
|
+
id: "codex-speed",
|
|
3890
|
+
order: 0,
|
|
3891
|
+
locale: LOCALE_NS,
|
|
3892
|
+
inject: (sessionId) => ({
|
|
3893
|
+
loadSpeed: createSpeedLoader(connection, models, sessionId),
|
|
3894
|
+
setSpeed: createSpeedSetter(connection, sessionId, models)
|
|
3895
|
+
})
|
|
3896
|
+
}, SpeedSelect));
|
|
3897
|
+
ctx.inject(["commandUi"], (scope) => {
|
|
3898
|
+
const command = scope.get("commandUi");
|
|
3899
|
+
scope.effect(() => command.register({
|
|
3900
|
+
name: "fast",
|
|
3901
|
+
description: fastCommandDescription(() => t("commandFast")),
|
|
3902
|
+
available: () => true,
|
|
3903
|
+
ui: {
|
|
3904
|
+
kind: "popupSelect",
|
|
3905
|
+
options: async (session) => {
|
|
3906
|
+
const state = await createSpeedLoader(connection, models, session.sessionId)();
|
|
3907
|
+
if (!state.visible) throw new Error(t("commandFastUnavailable"));
|
|
3908
|
+
return [{
|
|
3909
|
+
id: "standard",
|
|
3910
|
+
label: t("speedStandard"),
|
|
3911
|
+
detail: t("speedStandardDescription")
|
|
3912
|
+
}, {
|
|
3913
|
+
id: "fast",
|
|
3914
|
+
label: t("speedFast"),
|
|
3915
|
+
detail: t("speedFastDescription")
|
|
3916
|
+
}].map((option) => ({
|
|
3917
|
+
...option,
|
|
3918
|
+
active: option.id === state.tier
|
|
3919
|
+
}));
|
|
3920
|
+
},
|
|
3921
|
+
onSelect: async (option, session) => {
|
|
3922
|
+
await createSpeedSetter(connection, session.sessionId, models)(option.id === "fast" ? "fast" : "standard");
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}), "dsh-plugin-subscription-usage: /fast");
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
//#endregion
|
|
3930
|
+
exports.apply = apply;
|
|
3931
|
+
exports.inject = inject;
|
|
3932
|
+
return module.exports; } });
|
|
3933
|
+
//# sourceMappingURL=client.js.map
|