dsh-subscriptions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
package/lib/client.js ADDED
@@ -0,0 +1,4472 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-subscriptions", 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 __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
+ __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
32
+ let react_dom = require("react-dom");
33
+ react_dom = __toESM(react_dom);
34
+
35
+ //#region src/client/locales.ts
36
+ /** Copy dictionaries for the Subscriptions settings section. */
37
+ /** English strings (the key-set source of truth for this pair). */
38
+ const en = {
39
+ accountsManage: "Manage",
40
+ accountsTitle: "Manage {provider} accounts",
41
+ accountsHint: "Choose which accounts serve pooled LLM requests. Pool exclusion applies only to LLM routing, not tools or session-wide isolation.",
42
+ accountsLoading: "Loading accounts…",
43
+ accountsLoadFailed: "Could not load account settings: {message}",
44
+ accountsSaveFailed: "Could not save changes: {message}",
45
+ accountsEmpty: "No connected accounts. Add an account on the provider card first.",
46
+ accountsAlias: "Alias (optional)",
47
+ accountsPoolEnabled: "Include in the LLM account pool",
48
+ accountsIndependent: "Show an independent entry in the model picker",
49
+ accountsIndependentHint: "Independent LLM routes use only this account, with no fallback to another account. This does not isolate tools or the whole session.",
50
+ accountsModels: "Pool model allowlist",
51
+ accountsModelsAll: "All models",
52
+ accountsModelsCount: "{count} selected",
53
+ accountsModelsHint: "Applies only to pooled LLM routing, not independent entries. An empty selection allows no models. Turning off pool participation keeps this selection for later.",
54
+ accountsModelsAutomatic: "Allow all models, including future models",
55
+ accountsNoModels: "No models reported for this account.",
56
+ accountsCatalogUnavailable: "Model discovery is currently unavailable. Saved model selections are preserved.",
57
+ modelsPartialSave: "Some reasoning defaults were saved. Remaining changes are still in the draft; retry to finish.",
58
+ modelsEdit: "Edit model list",
59
+ modelsHint: "Configure model visibility, default reasoning effort, context and tools here; Save changes below submits them together with the account settings. Hidden models remain usable by existing sessions; tool changes apply to new sessions.",
60
+ modelsAutomatic: "Automatically show all models, including new models",
61
+ modelsSelectAll: "Select all",
62
+ modelsSelectNone: "Clear selection",
63
+ modelsContext: "Context (tokens)",
64
+ modelsContextBounds: "Default {default} · Maximum {max}",
65
+ modelsContextHint: "Leave blank to follow the provider. The requested window is capped by the account’s advertised maximum; account pools use the smallest member window. This controls local history and compaction, not the API’s capacity.",
66
+ modelsContextInvalid: "Enter a positive whole number of tokens for {model}.",
67
+ modelsTools: "Provider tools",
68
+ modelsToolsHint: "Changes apply only to sessions created after saving. Existing sessions keep their tools. Shared tools remain available if any supporting provider is enabled.",
69
+ modelsImage: "Image generation",
70
+ modelsVideo: "Video generation",
71
+ modelsSearch: "X search",
72
+ modelsWebSearch: "Web search",
73
+ modelsSave: "Save changes",
74
+ modelsUnavailable: "Currently unavailable",
75
+ nav: "Subscriptions",
76
+ intro: "Log a subscription provider in or out. Login opens the provider’s authorization page in a new tab; headless setups can paste the callback URL or code instead.",
77
+ unavailable: "Connection unavailable; subscription status cannot be loaded.",
78
+ checking: "Checking…",
79
+ loginInProgress: "Login in progress…",
80
+ notLoggedIn: "Not logged in",
81
+ loggedInCount: "{count} account(s) connected",
82
+ clientVersion: "CLI {version} · {source}",
83
+ clientVersionNpm: "latest on npm",
84
+ clientVersionLocal: "local CLI",
85
+ clientVersionFallback: "built-in",
86
+ clientVersionConfig: "configured",
87
+ clientVersionHint: "Requests present this CLI version, and newer models may require a recent one. It is the latest release on npm, or the local CLI / built-in version while npm is unreachable. Refreshing the model list checks again.",
88
+ accountExpires: "expires {date}",
89
+ defaultBadge: "Default",
90
+ setDefault: "Set as default",
91
+ addAccount: "Add account",
92
+ addAccountOAuth: "Browser authorization",
93
+ addAccountKeychain: "Import Claude Code",
94
+ addAccountHint: "Browser authorization signs in whichever account the browser currently uses — switch accounts there first (or use an incognito window with the manual code below) to add a different one.",
95
+ login: "Log in",
96
+ cancel: "Cancel",
97
+ logout: "Log out",
98
+ logoutAccountConfirm: "Log out {account} of {provider}?",
99
+ manualSummary: "Browser flow not working? Paste the callback URL or code",
100
+ manualPlaceholder: "Paste the callback URL or code",
101
+ submit: "Submit",
102
+ loginMissingUrl: "login answered without an authorizeUrl",
103
+ deviceCodePrompt: "Enter this code on the GitHub verification page:",
104
+ deviceCodeCopy: "Copy code",
105
+ deviceCodeCopied: "Copied",
106
+ deviceCodeOpenPage: "Open GitHub verification page",
107
+ usageTitle: "Usage",
108
+ usageRefresh: "Refresh",
109
+ usageLoading: "Loading usage…",
110
+ usageEmpty: "No usage windows reported.",
111
+ usageError: "Usage lookup failed: {message}",
112
+ usageSession: "5-hour window",
113
+ usageWeekly: "Weekly",
114
+ usageMonthly: "Monthly",
115
+ usageWindow: "Window",
116
+ usageResets: "resets {date}",
117
+ usagePlan: "Plan: {plan}",
118
+ externalUsageTitle: "Built-in provider usage",
119
+ externalUsageConnected: "API key configured",
120
+ externalUsageNotConfigured: "Not configured",
121
+ externalUsageApiKey: "API key",
122
+ externalUsageConfigure: "No API key found at {ref}. Add one in Settings → Models.",
123
+ externalUsageUnavailable: "Usage status unavailable: {message}",
124
+ cursorTitle: "Cursor",
125
+ cursorOpenLogin: "Open Cursor sign-in",
126
+ cursorWaiting: "Waiting for Cursor sign-in…",
127
+ cursorConnected: "Connected",
128
+ cursorManageTitle: "Manage Cursor",
129
+ cursorManageSignIn: "Sign in to see the models available to this Cursor account.",
130
+ cursorAccountsHint: "Cursor uses one connected account. Choose which of its models appear in the model picker.",
131
+ cursorManageHint: "Choose model visibility here. Refresh fetches the current models from Cursor; Save changes updates the model picker.",
132
+ cursorFilterModels: "Filter models",
133
+ cursorModelsLoading: "Loading Cursor models…",
134
+ cursorModelsError: "Could not load Cursor models: {message}",
135
+ cursorModelsCount: "{count} models available",
136
+ cursorIncluded: "Included usage",
137
+ cursorModels: "Cursor Models",
138
+ cursorOtherModels: "Other Models",
139
+ cursorIncludedRequests: "Included requests",
140
+ modelDefaultsTitle: "Default reasoning effort",
141
+ modelDefaultsHint: "The model picker preselects this level when you switch to the model; models without levels always follow the provider.",
142
+ modelDefaultsFollowProvider: "Follow provider",
143
+ modelDefaultsNoLevels: "{count} model(s) advertise no reasoning levels and always follow the provider.",
144
+ modelDefaultsLoading: "Loading models…",
145
+ modelDefaultsLoadFailed: "Failed to load models: {message}",
146
+ modelDefaultsRetry: "Retry",
147
+ modelDefaultsRefresh: "Refresh model lists (all subscriptions)",
148
+ modelDefaultsSaving: "Saving…",
149
+ modelDefaultsSaveFailedNamed: "Save failed for {model}: {message}",
150
+ modelDefaultsSummary: "{total} model(s) · {configured} overridden",
151
+ modelDefaultsSummaryNone: "{total} model(s) · following the provider",
152
+ modelDefaultsSummaryEmpty: "No model advertises reasoning levels",
153
+ modelDefaultsFilterPlaceholder: "Filter models",
154
+ modelDefaultsFilterEmpty: "No model matches “{query}”.",
155
+ generating: "Generating image…",
156
+ image: "image",
157
+ viewImage: "View image",
158
+ viewImageNamed: "View {name}",
159
+ imageLoading: "Loading…",
160
+ imageLoadFailed: "Retry",
161
+ imagePreview: "Image preview",
162
+ imageClose: "Close",
163
+ generatingVideo: "Generating video…",
164
+ videoLoading: "Loading video…",
165
+ videoLoadFailed: "Video failed to load: {message}",
166
+ usageBadgeTitle: "Provider usage",
167
+ usageBadgeDisplay: "Status-bar quota display",
168
+ usageBadgeDisplayRecent: "Current / most recent provider with usage",
169
+ usageBadgeDisplayHidden: "Hidden",
170
+ usageBadgeDisplayHint: "Saved automatically in this browser. One provider is shown at a time. Models without a quota source keep the most recent provider selected in this view, or show nothing. Hiding does not affect usage details here.",
171
+ usageBadgeDisplaySaveFailed: "Could not save the display preference. Check browser storage permissions.",
172
+ usageBadgeCurrent: "current",
173
+ usageBadgeModelCount: "{count} model quotas",
174
+ usageBadgeModelUnavailable: "Current model quota unavailable",
175
+ usageBadgeMoreWindows: "More quota windows ({count})",
176
+ speed: "Speed",
177
+ speedStandard: "Standard",
178
+ speedStandardDescription: "Default speed",
179
+ speedFast: "Fast",
180
+ speedFastDescription: "1.5x speed, more usage",
181
+ commandFast: "Switch the Codex speed tier (Standard/Fast)",
182
+ commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one"
183
+ };
184
+ /** zh strings, one per {@link en} key. */
185
+ const zh = {
186
+ accountsManage: "管理",
187
+ accountsTitle: "管理 {provider} 账号",
188
+ accountsHint: "选择参与账号池 LLM 请求的账号。退出账号池仅影响 LLM 路由,不影响工具,也不代表整个会话隔离。",
189
+ accountsLoading: "加载账号中…",
190
+ accountsLoadFailed: "账号设置加载失败:{message}",
191
+ accountsSaveFailed: "保存失败:{message}",
192
+ accountsEmpty: "暂无已连接账号,请先在服务商卡片中添加账号。",
193
+ accountsAlias: "别名(可选)",
194
+ accountsPoolEnabled: "参与 LLM 账号池",
195
+ accountsIndependent: "在模型选择器中显示独立入口",
196
+ accountsIndependentHint: "独立 LLM 路由仅使用此账号,失败时不会回退到其他账号。这不代表工具或整个会话的隔离。",
197
+ accountsModels: "账号池模型白名单",
198
+ accountsModelsAll: "全部模型",
199
+ accountsModelsCount: "已选 {count} 个",
200
+ accountsModelsHint: "仅影响账号池 LLM 路由,不限制独立入口。清空选择表示不允许任何模型。关闭参与账号池后仍保留此选择。",
201
+ accountsModelsAutomatic: "允许全部模型(包含以后新增的模型)",
202
+ accountsNoModels: "此账号暂未返回模型。",
203
+ accountsCatalogUnavailable: "暂时无法获取模型目录,已保存的模型选择会被保留。",
204
+ modelsPartialSave: "部分推理档已保存,其余更改仍保留在草稿中,请重试完成保存。",
205
+ modelsEdit: "编辑模型列表",
206
+ modelsHint: "在此配置模型显示、默认推理档、上下文与工具,底部的「保存更改」会连同账号设置一起提交。隐藏模型不影响已有会话;工具开关对新建会话生效。",
207
+ modelsAutomatic: "自动显示全部模型(包含以后新增的模型)",
208
+ modelsSelectAll: "全选",
209
+ modelsSelectNone: "清空选择",
210
+ modelsContext: "上下文(token)",
211
+ modelsContextBounds: "默认 {default} · 最大 {max}",
212
+ modelsContextHint: "留空跟随服务商。配置值按账号返回的最大窗口限制;账号池取各成员的最小窗口。此设置控制本地历史保留和压缩时机,不会扩大 API 本身的容量。",
213
+ modelsContextInvalid: "请为 {model} 输入正整数 token 数。",
214
+ modelsTools: "Provider 工具",
215
+ modelsToolsHint: "仅对保存后新建的会话生效,已有会话保留原工具。任一支持的服务商启用共享工具时,该工具仍会显示。",
216
+ modelsImage: "图片生成",
217
+ modelsVideo: "视频生成",
218
+ modelsSearch: "X 搜索",
219
+ modelsWebSearch: "网页搜索",
220
+ modelsSave: "保存更改",
221
+ modelsUnavailable: "当前不可用",
222
+ nav: "订阅",
223
+ intro: "在此登录或退出订阅服务商。点击登录会在新标签页打开服务商的授权页面;无浏览器环境可改为粘贴回调 URL 或授权码。",
224
+ unavailable: "连接不可用,无法加载订阅状态。",
225
+ checking: "查询中…",
226
+ loginInProgress: "登录中…",
227
+ notLoggedIn: "未登录",
228
+ loggedInCount: "已连接 {count} 个账号",
229
+ clientVersion: "CLI {version} · {source}",
230
+ clientVersionNpm: "npm 最新",
231
+ clientVersionLocal: "本机 CLI",
232
+ clientVersionFallback: "内置版本",
233
+ clientVersionConfig: "手动配置",
234
+ clientVersionHint: "请求以这个 CLI 版本号发出,新模型可能要求较新的版本。默认使用 npm 上的最新版本;npm 无法访问时使用本机 CLI 或内置版本。刷新模型列表会重新检查。",
235
+ accountExpires: "过期时间 {date}",
236
+ defaultBadge: "默认",
237
+ setDefault: "设为默认",
238
+ addAccount: "添加账号",
239
+ addAccountOAuth: "浏览器授权",
240
+ addAccountKeychain: "导入 Claude Code",
241
+ addAccountHint: "浏览器授权以浏览器当前登录的账号为准;要添加不同账号,请先在浏览器里切换账号,或用无痕窗口走下方手动授权码。",
242
+ login: "登录",
243
+ cancel: "取消",
244
+ logout: "退出登录",
245
+ logoutAccountConfirm: "确定退出 {provider} 的账号 {account} 吗?",
246
+ manualSummary: "浏览器流程无法完成?粘贴回调 URL 或授权码",
247
+ manualPlaceholder: "粘贴回调 URL 或授权码",
248
+ submit: "提交",
249
+ loginMissingUrl: "login 响应缺少 authorizeUrl",
250
+ deviceCodePrompt: "在 GitHub 验证页面输入此验证码:",
251
+ deviceCodeCopy: "复制验证码",
252
+ deviceCodeCopied: "已复制",
253
+ deviceCodeOpenPage: "打开 GitHub 验证页面",
254
+ usageTitle: "用量",
255
+ usageRefresh: "刷新",
256
+ usageLoading: "用量加载中…",
257
+ usageEmpty: "服务商未返回任何用量窗口。",
258
+ usageError: "用量查询失败:{message}",
259
+ usageSession: "5 小时窗口",
260
+ usageWeekly: "每周",
261
+ usageMonthly: "每月",
262
+ usageWindow: "窗口",
263
+ usageResets: "{date} 重置",
264
+ usagePlan: "计划:{plan}",
265
+ externalUsageTitle: "内置供应商用量",
266
+ externalUsageConnected: "已配置 API 密钥",
267
+ externalUsageNotConfigured: "未配置",
268
+ externalUsageApiKey: "API 密钥",
269
+ externalUsageConfigure: "未在 {ref} 找到 API 密钥。请在「设置 → 模型」中添加。",
270
+ externalUsageUnavailable: "无法获取用量状态:{message}",
271
+ cursorTitle: "Cursor",
272
+ cursorOpenLogin: "打开 Cursor 登录页面",
273
+ cursorWaiting: "等待 Cursor 登录…",
274
+ cursorConnected: "已连接",
275
+ cursorManageTitle: "管理 Cursor",
276
+ cursorManageSignIn: "登录后可查看此 Cursor 账号可用的模型。",
277
+ cursorAccountsHint: "Cursor 使用一个已连接账号。可在此选择模型选择器中显示的模型。",
278
+ cursorManageHint: "在此选择模型是否显示。刷新可从 Cursor 获取当前模型;保存更改后更新模型选择器。",
279
+ cursorFilterModels: "筛选模型",
280
+ cursorModelsLoading: "正在加载 Cursor 模型…",
281
+ cursorModelsError: "无法加载 Cursor 模型:{message}",
282
+ cursorModelsCount: "可用模型:{count} 个",
283
+ cursorIncluded: "套餐内用量",
284
+ cursorModels: "Cursor 模型",
285
+ cursorOtherModels: "其他模型",
286
+ cursorIncludedRequests: "套餐内请求",
287
+ modelDefaultsTitle: "默认推理档",
288
+ modelDefaultsHint: "切换到该模型时,模型选择器会预选此档位;没有推理档的模型始终跟随服务商默认。",
289
+ modelDefaultsFollowProvider: "跟随服务商",
290
+ modelDefaultsNoLevels: "{count} 个模型未声明推理档,始终跟随服务商默认。",
291
+ modelDefaultsLoading: "加载模型中…",
292
+ modelDefaultsLoadFailed: "模型加载失败:{message}",
293
+ modelDefaultsRetry: "重试",
294
+ modelDefaultsRefresh: "刷新模型列表(全部订阅)",
295
+ modelDefaultsSaving: "保存中…",
296
+ modelDefaultsSaveFailedNamed: "{model} 保存失败:{message}",
297
+ modelDefaultsSummary: "{total} 个模型 · {configured} 个已覆盖",
298
+ modelDefaultsSummaryNone: "{total} 个模型 · 全部跟随服务商",
299
+ modelDefaultsSummaryEmpty: "没有模型声明推理档",
300
+ modelDefaultsFilterPlaceholder: "筛选模型",
301
+ modelDefaultsFilterEmpty: "没有匹配「{query}」的模型。",
302
+ generating: "正在生成图片…",
303
+ image: "图片",
304
+ viewImage: "查看图片",
305
+ viewImageNamed: "查看 {name}",
306
+ imageLoading: "加载中…",
307
+ imageLoadFailed: "重试",
308
+ imagePreview: "图片预览",
309
+ imageClose: "关闭",
310
+ generatingVideo: "正在生成视频…",
311
+ videoLoading: "视频加载中…",
312
+ videoLoadFailed: "视频加载失败:{message}",
313
+ usageBadgeTitle: "服务商用量",
314
+ usageBadgeDisplay: "状态栏额度显示",
315
+ usageBadgeDisplayRecent: "仅显示当前/最近使用的用量服务商",
316
+ usageBadgeDisplayHidden: "不显示",
317
+ usageBadgeDisplayHint: "自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。关闭不影响本页的额度详情。",
318
+ usageBadgeDisplaySaveFailed: "显示设置保存失败,请检查浏览器存储权限。",
319
+ usageBadgeCurrent: "当前",
320
+ usageBadgeModelCount: "{count} 个模型配额",
321
+ usageBadgeModelUnavailable: "当前模型暂无配额数据",
322
+ usageBadgeMoreWindows: "更多配额窗口({count})",
323
+ speed: "速度",
324
+ speedStandard: "标准",
325
+ speedStandardDescription: "默认速度",
326
+ speedFast: "快速",
327
+ speedFastDescription: "约 1.5 倍速度,消耗更多用量",
328
+ commandFast: "切换 Codex 速度档(标准/快速)",
329
+ commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
330
+ };
331
+
332
+ //#endregion
333
+ //#region src/client/subscriptions-rpc.ts
334
+ /**
335
+ * The node half mounts every endpoint as an exact POST route on the shared
336
+ * `/api` channel (`/api/subscriptions-auth.<endpoint>`), so the browser
337
+ * reaches it through the `/api` channel with a prefixed endpoint name.
338
+ * A dedicated channel is no longer used: since dsh 0.1.5 `rpc.handle`
339
+ * cannot register one from a plugin (see `src/auth/rpc.ts`).
340
+ */
341
+ const SUBSCRIPTIONS_AUTH_CHANNEL = "/api";
342
+ const SUBSCRIPTIONS_AUTH_PREFIX = "subscriptions-auth.";
343
+ /** Business error returned by a `subscriptions-auth` endpoint (error branch message). */
344
+ var SubscriptionsAuthError = class extends Error {};
345
+ /**
346
+ * Call one `subscriptions-auth` endpoint and unwrap the business result.
347
+ * Shared by the settings section, the composer Speed toggle, the usage
348
+ * badge, and the image/video toolviews.
349
+ * @param rpc - Connection RPC caller.
350
+ * @param endpoint - endpoint name (`status`, `usage`, `image`, ...).
351
+ * @param payload - endpoint-owned request payload.
352
+ * @returns the success value, cast by the caller to the endpoint's shape.
353
+ */
354
+ async function callSubscriptionsAuth(rpc, endpoint, payload) {
355
+ let result;
356
+ try {
357
+ result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL, `${SUBSCRIPTIONS_AUTH_PREFIX}${endpoint}`, payload);
358
+ } catch (error) {
359
+ throw new SubscriptionsAuthError(error instanceof Error ? error.message : String(error));
360
+ }
361
+ if (!result.ok) throw new SubscriptionsAuthError(result.error.message);
362
+ return result.value;
363
+ }
364
+
365
+ //#endregion
366
+ //#region src/client/account-preferences.ts
367
+ /** Missing allowlist follows discovery; explicit [] must remain empty. */
368
+ function accountPoolSelection(account, models) {
369
+ return new Set(account.poolModels ?? models.map((model) => model.id));
370
+ }
371
+ /** Retain unavailable saved IDs so a temporary catalog failure cannot erase them. */
372
+ function accountModelRows(account, preferences) {
373
+ const known = new Set(account.models.map((model) => model.id));
374
+ return [...account.models.map((model) => ({
375
+ ...model,
376
+ unavailable: false
377
+ })), ...(preferences.poolModels ?? []).filter((id) => !known.has(id)).map((id) => ({
378
+ id,
379
+ name: id,
380
+ unavailable: true
381
+ }))];
382
+ }
383
+ /** The model editor owns everything except the separately managed accounts. */
384
+ function mergeLatestAccounts(draft, latest) {
385
+ const { accounts: _stale,...settings } = draft;
386
+ return {
387
+ ...settings,
388
+ ...latest.accounts === void 0 ? {} : { accounts: latest.accounts }
389
+ };
390
+ }
391
+ /** Only touched accounts replace their latest preferences; other settings survive. */
392
+ function mergeAccountChanges(latest, changes) {
393
+ return {
394
+ ...latest,
395
+ accounts: {
396
+ ...latest.accounts,
397
+ ...changes
398
+ }
399
+ };
400
+ }
401
+
402
+ //#endregion
403
+ //#region src/client/ProviderModelEditor.tsx
404
+ const border$1 = "1px solid var(--dsw-alias-border-l2, #ddd)";
405
+ const control$1 = {
406
+ font: "inherit",
407
+ color: "inherit",
408
+ background: "transparent",
409
+ border: border$1,
410
+ borderRadius: 8,
411
+ padding: "6px 10px",
412
+ minWidth: 0
413
+ };
414
+ const actions$1 = {
415
+ display: "flex",
416
+ flexWrap: "wrap",
417
+ gap: 8,
418
+ alignItems: "center"
419
+ };
420
+ /**
421
+ * Embedded section of the account manager dialog. It owns the model draft
422
+ * (visibility, effort, context, tools) but not its submission: the dialog
423
+ * collects the draft through the handle and saves it with the account edits
424
+ * in one go, so there is a single Save/Cancel pair.
425
+ */
426
+ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1({ provider, rpc, t, disabled = false, onDirtyChange }, ref) {
427
+ const [catalog, setCatalog] = (0, react.useState)();
428
+ const [draft, setDraft] = (0, react.useState)({});
429
+ const [contexts, setContexts] = (0, react.useState)({});
430
+ const [efforts, setEfforts] = (0, react.useState)({});
431
+ const [query, setQuery] = (0, react.useState)("");
432
+ const [busy, setBusy] = (0, react.useState)(false);
433
+ const [dirty, setDirty] = (0, react.useState)(false);
434
+ const [error, setError] = (0, react.useState)("");
435
+ const generation = (0, react.useRef)(0);
436
+ (0, react.useEffect)(() => () => {
437
+ generation.current++;
438
+ }, []);
439
+ (0, react.useEffect)(() => {
440
+ onDirtyChange?.(dirty);
441
+ }, [dirty]);
442
+ function reset(data) {
443
+ setCatalog(data);
444
+ setDraft(data.settings);
445
+ setEfforts(Object.fromEntries(data.models.map((model) => [model.id, model.configured ?? ""])));
446
+ setContexts(Object.fromEntries(Object.entries(data.settings.contextWindows ?? {}).map(([id, value]) => [id, String(value)])));
447
+ setDirty(false);
448
+ }
449
+ async function load(force = false) {
450
+ const request = ++generation.current;
451
+ setBusy(true);
452
+ setError("");
453
+ try {
454
+ const data = await callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", {
455
+ provider,
456
+ force
457
+ });
458
+ if (generation.current === request) reset(data);
459
+ } catch (error$1) {
460
+ if (generation.current === request) setError(String(error$1 instanceof Error ? error$1.message : error$1));
461
+ } finally {
462
+ if (generation.current === request) setBusy(false);
463
+ }
464
+ }
465
+ function edit(next) {
466
+ setDraft(next);
467
+ setDirty(true);
468
+ }
469
+ (0, react.useEffect)(() => {
470
+ load();
471
+ }, [provider, rpc]);
472
+ (0, react.useImperativeHandle)(ref, () => ({ collect() {
473
+ if (!catalog || !dirty) return { efforts: [] };
474
+ const windows = Object.create(null);
475
+ for (const [model, text] of Object.entries(contexts)) {
476
+ if (!text.trim()) continue;
477
+ const value = Number(text);
478
+ if (!/^\d+$/.test(text.trim()) || !Number.isSafeInteger(value) || value <= 0) {
479
+ setError(t("modelsContextInvalid", { model }));
480
+ return;
481
+ }
482
+ windows[model] = value;
483
+ }
484
+ setError("");
485
+ const { accounts: _accounts,...settings } = {
486
+ ...draft,
487
+ ...provider === "codex" ? { contextWindows: windows } : {}
488
+ };
489
+ return {
490
+ settings,
491
+ efforts: catalog.models.flatMap((model) => {
492
+ const effort = efforts[model.id] ?? "";
493
+ return effort === (model.configured ?? "") || !model.efforts?.length ? [] : [{
494
+ model: model.id,
495
+ effort
496
+ }];
497
+ })
498
+ };
499
+ } }), [
500
+ catalog,
501
+ dirty,
502
+ draft,
503
+ contexts,
504
+ efforts,
505
+ provider
506
+ ]);
507
+ const allModels = catalog?.models ?? [];
508
+ const known = new Set(allModels.map((model) => model.id));
509
+ const missing = (draft.visibleModels ?? []).filter((id) => !known.has(id)).map((id) => ({
510
+ id,
511
+ name: id
512
+ }));
513
+ const models = [...allModels, ...missing].filter((model) => `${model.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()));
514
+ const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
515
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
516
+ style: {
517
+ borderTop: border$1,
518
+ marginTop: 12,
519
+ paddingTop: 12
520
+ },
521
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
522
+ style: {
523
+ margin: 0,
524
+ fontSize: 15
525
+ },
526
+ children: t("modelsEdit")
527
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
528
+ style: {
529
+ display: "grid",
530
+ gap: 12,
531
+ marginTop: 12
532
+ },
533
+ children: [
534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
535
+ style: { margin: 0 },
536
+ children: t(provider === "cursor-subscription" ? "cursorManageHint" : "modelsHint")
537
+ }),
538
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
539
+ role: "alert",
540
+ style: {
541
+ margin: 0,
542
+ color: "var(--dsw-alias-state-error-primary, #b42318)"
543
+ },
544
+ children: error
545
+ }),
546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
547
+ style: actions$1,
548
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
549
+ type: "button",
550
+ style: control$1,
551
+ disabled: busy || disabled || dirty,
552
+ onClick: () => {
553
+ load(true);
554
+ },
555
+ children: t("usageRefresh")
556
+ }), busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
557
+ role: "status",
558
+ children: t("modelDefaultsLoading")
559
+ })]
560
+ }),
561
+ catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
562
+ disabled: busy || disabled,
563
+ style: {
564
+ border: 0,
565
+ padding: 0,
566
+ margin: 0,
567
+ minWidth: 0,
568
+ display: "grid",
569
+ gap: 12
570
+ },
571
+ children: [
572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
574
+ type: "checkbox",
575
+ checked: draft.visibleModels === void 0,
576
+ onChange: (event) => {
577
+ const next = { ...draft };
578
+ if (event.target.checked) delete next.visibleModels;
579
+ else next.visibleModels = allModels.map((model) => model.id);
580
+ edit(next);
581
+ }
582
+ }),
583
+ " ",
584
+ t("modelsAutomatic")
585
+ ] }),
586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
587
+ style: actions$1,
588
+ children: [
589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
590
+ style: {
591
+ ...control$1,
592
+ flex: "1 1 180px"
593
+ },
594
+ value: query,
595
+ onChange: (event) => setQuery(event.target.value),
596
+ placeholder: t("modelDefaultsFilterPlaceholder"),
597
+ "aria-label": t("modelDefaultsFilterPlaceholder")
598
+ }),
599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
600
+ type: "button",
601
+ style: control$1,
602
+ onClick: () => edit({
603
+ ...draft,
604
+ visibleModels: allModels.map((model) => model.id)
605
+ }),
606
+ children: t("modelsSelectAll")
607
+ }),
608
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
609
+ type: "button",
610
+ style: control$1,
611
+ onClick: () => edit({
612
+ ...draft,
613
+ visibleModels: []
614
+ }),
615
+ children: t("modelsSelectNone")
616
+ })
617
+ ]
618
+ }),
619
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
620
+ style: {
621
+ maxHeight: 360,
622
+ overflowY: "auto",
623
+ display: "grid",
624
+ gap: 8
625
+ },
626
+ children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
627
+ style: {
628
+ borderBottom: border$1,
629
+ padding: "8px 2px",
630
+ display: "grid",
631
+ gap: 8
632
+ },
633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
634
+ style: { overflowWrap: "anywhere" },
635
+ children: [
636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
637
+ type: "checkbox",
638
+ checked: selected.has(model.id),
639
+ onChange: (event) => {
640
+ const next = new Set(selected);
641
+ if (event.target.checked) next.add(model.id);
642
+ else next.delete(model.id);
643
+ edit({
644
+ ...draft,
645
+ visibleModels: [...next]
646
+ });
647
+ }
648
+ }),
649
+ " ",
650
+ model.name,
651
+ !known.has(model.id) && ` (${t("modelsUnavailable")})`
652
+ ]
653
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
654
+ style: actions$1,
655
+ children: [(model.efforts?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
656
+ style: actions$1,
657
+ children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
658
+ style: control$1,
659
+ "aria-label": `${model.name} ${t("modelDefaultsTitle")}`,
660
+ value: efforts[model.id] ?? "",
661
+ onChange: (event) => {
662
+ setEfforts({
663
+ ...efforts,
664
+ [model.id]: event.target.value
665
+ });
666
+ setDirty(true);
667
+ },
668
+ children: [
669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
670
+ value: "",
671
+ children: t("modelDefaultsFollowProvider")
672
+ }),
673
+ model.configured && !model.efforts?.some((effort) => effort.id === model.configured) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
674
+ value: model.configured,
675
+ disabled: true,
676
+ children: [
677
+ model.configured,
678
+ " (",
679
+ t("modelsUnavailable"),
680
+ ")"
681
+ ]
682
+ }),
683
+ model.efforts?.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
684
+ value: effort.id,
685
+ children: effort.name
686
+ }, effort.id))
687
+ ]
688
+ })]
689
+ }), provider === "codex" && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
690
+ style: actions$1,
691
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
692
+ style: actions$1,
693
+ children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
694
+ style: {
695
+ ...control$1,
696
+ width: 140
697
+ },
698
+ inputMode: "numeric",
699
+ value: contexts[model.id] ?? "",
700
+ "aria-label": `${model.name} ${t("modelsContext")}`,
701
+ placeholder: String(model.defaultContextWindow),
702
+ onChange: (event) => {
703
+ setContexts({
704
+ ...contexts,
705
+ [model.id]: event.target.value
706
+ });
707
+ setDirty(true);
708
+ }
709
+ })]
710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextBounds", {
711
+ default: model.defaultContextWindow,
712
+ max: model.maxContextWindow
713
+ }) })]
714
+ })]
715
+ })]
716
+ }, model.id)), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelDefaultsFilterEmpty", { query }) })]
717
+ }),
718
+ provider === "codex" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
719
+ catalog.tools.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
720
+ style: {
721
+ display: "grid",
722
+ gap: 8
723
+ },
724
+ children: [
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("modelsTools") }),
726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsToolsHint") }),
727
+ catalog.tools.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
729
+ type: "checkbox",
730
+ checked: draft.tools?.[tool] !== false,
731
+ onChange: (event) => edit({
732
+ ...draft,
733
+ tools: {
734
+ ...draft.tools,
735
+ [tool]: event.target.checked
736
+ }
737
+ })
738
+ }),
739
+ " ",
740
+ t(tool === "image_generate" ? "modelsImage" : tool === "video_generate" ? "modelsVideo" : tool === "web_search" ? "modelsWebSearch" : "modelsSearch")
741
+ ] }, tool))
742
+ ]
743
+ })
744
+ ]
745
+ })
746
+ ]
747
+ })]
748
+ });
749
+ });
750
+
751
+ //#endregion
752
+ //#region src/client/ProviderAccountManager.tsx
753
+ const border = "1px solid var(--dsw-alias-border-l2)";
754
+ const control = {
755
+ font: "inherit",
756
+ color: "inherit",
757
+ background: "var(--dsw-alias-bg-layer-1)",
758
+ border,
759
+ borderRadius: 8,
760
+ padding: "7px 12px",
761
+ minWidth: 0
762
+ };
763
+ const button = {
764
+ ...control,
765
+ cursor: "pointer"
766
+ };
767
+ const hint = {
768
+ margin: 0,
769
+ fontSize: 12,
770
+ lineHeight: 1.6,
771
+ color: "var(--dsw-alias-label-tertiary)"
772
+ };
773
+ const stack = {
774
+ display: "grid",
775
+ gap: 12,
776
+ minWidth: 0
777
+ };
778
+ const actions = {
779
+ display: "flex",
780
+ flexWrap: "wrap",
781
+ gap: 8,
782
+ alignItems: "center"
783
+ };
784
+ /** Native top-layer dialog provides keyboard containment, Escape and focus restoration. */
785
+ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
786
+ const dialog = (0, react.useRef)(null);
787
+ const title = (0, react.useId)();
788
+ const description = (0, react.useId)();
789
+ const [catalog, setCatalog] = (0, react.useState)();
790
+ const [changes, setChanges] = (0, react.useState)({});
791
+ const [loading, setLoading] = (0, react.useState)(true);
792
+ const [saving, setSaving] = (0, react.useState)(false);
793
+ const [error, setError] = (0, react.useState)("");
794
+ const [saveError, setSaveError] = (0, react.useState)("");
795
+ const [attempt, setAttempt] = (0, react.useState)(0);
796
+ const [modelsDirty, setModelsDirty] = (0, react.useState)(false);
797
+ const alive = (0, react.useRef)(true);
798
+ const saveLock = (0, react.useRef)(false);
799
+ const editor = (0, react.useRef)(null);
800
+ (0, react.useEffect)(() => {
801
+ alive.current = true;
802
+ const element = dialog.current;
803
+ const previous = document.activeElement;
804
+ element.showModal();
805
+ return () => {
806
+ alive.current = false;
807
+ element.close();
808
+ if (previous instanceof HTMLElement && previous.isConnected) previous.focus();
809
+ };
810
+ }, []);
811
+ (0, react.useEffect)(() => {
812
+ let current = true;
813
+ setLoading(true);
814
+ setError("");
815
+ callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", { provider }).then((data) => {
816
+ if (current) setCatalog(data);
817
+ }).catch((error$1) => {
818
+ if (current) setError(t("accountsLoadFailed", { message: error$1 instanceof Error ? error$1.message : String(error$1) }));
819
+ }).finally(() => {
820
+ if (current) setLoading(false);
821
+ });
822
+ return () => {
823
+ current = false;
824
+ };
825
+ }, [
826
+ provider,
827
+ rpc,
828
+ attempt
829
+ ]);
830
+ function edit(key, next) {
831
+ setChanges((current) => ({
832
+ ...current,
833
+ [key]: next
834
+ }));
835
+ }
836
+ /**
837
+ * One submit for both drafts: the model editor's (visibility, effort,
838
+ * context, tools) and this dialog's account edits. A failure keeps the
839
+ * dialog open with every edit intact.
840
+ */
841
+ async function save() {
842
+ if (saveLock.current) return;
843
+ const models = editor.current === null ? { efforts: [] } : editor.current.collect();
844
+ if (models === void 0) return;
845
+ saveLock.current = true;
846
+ setSaving(true);
847
+ setSaveError("");
848
+ let savedEfforts = 0;
849
+ try {
850
+ if (provider !== "cursor-subscription") for (const { model, effort } of models.efforts) {
851
+ await callSubscriptionsAuth(rpc, "setModelDefault", {
852
+ provider,
853
+ model,
854
+ ...effort ? { effort } : {}
855
+ });
856
+ savedEfforts++;
857
+ if (!alive.current) return;
858
+ }
859
+ const latest = await callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", { provider });
860
+ if (!alive.current) return;
861
+ if (provider === "cursor-subscription") await callSubscriptionsAuth(rpc, "cursorSetSettings", { settings: models.settings ?? latest.settings });
862
+ else await callSubscriptionsAuth(rpc, "setProviderSettings", {
863
+ provider,
864
+ settings: mergeAccountChanges(models.settings === void 0 ? latest.settings : mergeLatestAccounts(models.settings, latest.settings), changes)
865
+ });
866
+ if (alive.current) onClose();
867
+ } catch (error$1) {
868
+ if (alive.current) {
869
+ const message$1 = error$1 instanceof Error ? error$1.message : String(error$1);
870
+ setSaveError((savedEfforts ? t("modelsPartialSave") + " " : "") + t("accountsSaveFailed", { message: message$1 }));
871
+ }
872
+ } finally {
873
+ saveLock.current = false;
874
+ if (alive.current) setSaving(false);
875
+ }
876
+ }
877
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
878
+ ref: dialog,
879
+ "aria-labelledby": title,
880
+ "aria-describedby": description,
881
+ onCancel: (event) => {
882
+ if (saveLock.current) event.preventDefault();
883
+ },
884
+ onClose,
885
+ style: {
886
+ width: 620,
887
+ maxWidth: "calc(100vw - 32px)",
888
+ maxHeight: "calc(100dvh - 32px)",
889
+ boxSizing: "border-box",
890
+ padding: 0,
891
+ border,
892
+ borderRadius: 16,
893
+ color: "var(--dsw-alias-label-primary)",
894
+ background: "var(--dsw-alias-bg-layer-1)",
895
+ boxShadow: "0 20px 70px #0004",
896
+ overflow: "auto"
897
+ },
898
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
899
+ style: {
900
+ ...stack,
901
+ padding: 20
902
+ },
903
+ children: [
904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
905
+ style: {
906
+ ...actions,
907
+ justifyContent: "space-between"
908
+ },
909
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
910
+ id: title,
911
+ style: {
912
+ margin: 0,
913
+ fontSize: 18
914
+ },
915
+ children: t("accountsTitle", { provider: name })
916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
917
+ type: "button",
918
+ autoFocus: true,
919
+ style: button,
920
+ disabled: saving,
921
+ onClick: onClose,
922
+ children: t("imageClose")
923
+ })]
924
+ }),
925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
926
+ id: description,
927
+ style: hint,
928
+ children: t(provider === "cursor-subscription" ? "cursorAccountsHint" : "accountsHint")
929
+ }),
930
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
931
+ role: "alert",
932
+ style: {
933
+ ...hint,
934
+ color: "var(--dsw-alias-state-error-primary)"
935
+ },
936
+ children: error
937
+ }),
938
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
939
+ role: "status",
940
+ style: hint,
941
+ children: t("accountsLoading")
942
+ }),
943
+ !loading && !catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
944
+ type: "button",
945
+ style: button,
946
+ onClick: () => setAttempt((value) => value + 1),
947
+ children: t("modelDefaultsRetry")
948
+ }),
949
+ catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
950
+ disabled: saving || loading,
951
+ style: {
952
+ ...stack,
953
+ border: 0,
954
+ margin: 0,
955
+ padding: 0
956
+ },
957
+ children: [
958
+ catalog.accounts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
959
+ style: hint,
960
+ children: t(provider === "cursor-subscription" ? "cursorManageSignIn" : "accountsEmpty")
961
+ }),
962
+ provider === "cursor-subscription" && catalog.accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
963
+ style: {
964
+ border,
965
+ borderRadius: 12,
966
+ padding: 14
967
+ },
968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
969
+ style: {
970
+ padding: "0 6px",
971
+ fontWeight: 600,
972
+ fontSize: 14
973
+ },
974
+ children: name
975
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
976
+ style: hint,
977
+ children: t("cursorConnected")
978
+ })]
979
+ }),
980
+ provider !== "cursor-subscription" && catalog.accounts.map((account) => {
981
+ const preferences = changes[account.key] ?? catalog.settings.accounts?.[account.key] ?? {};
982
+ const selected = accountPoolSelection(preferences, account.models);
983
+ const models = accountModelRows(account, preferences);
984
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
985
+ style: {
986
+ ...stack,
987
+ border,
988
+ borderRadius: 12,
989
+ padding: 14,
990
+ margin: 0
991
+ },
992
+ children: [
993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
994
+ style: {
995
+ padding: "0 6px",
996
+ fontWeight: 600,
997
+ fontSize: 14,
998
+ overflowWrap: "anywhere",
999
+ maxWidth: "100%"
1000
+ },
1001
+ children: account.label
1002
+ }),
1003
+ account.unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1004
+ style: hint,
1005
+ children: t("accountsCatalogUnavailable")
1006
+ }),
1007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1008
+ style: {
1009
+ ...stack,
1010
+ gap: 6
1011
+ },
1012
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1013
+ style: { fontSize: 12 },
1014
+ children: t("accountsAlias")
1015
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1016
+ style: control,
1017
+ value: preferences.alias ?? "",
1018
+ placeholder: account.label,
1019
+ onChange: (event) => {
1020
+ const next = { ...preferences };
1021
+ if (event.target.value.trim()) next.alias = event.target.value;
1022
+ else delete next.alias;
1023
+ edit(account.key, next);
1024
+ }
1025
+ })]
1026
+ }),
1027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
1028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1029
+ type: "checkbox",
1030
+ checked: preferences.poolEnabled !== false,
1031
+ onChange: (event) => edit(account.key, {
1032
+ ...preferences,
1033
+ poolEnabled: event.target.checked
1034
+ })
1035
+ }),
1036
+ " ",
1037
+ t("accountsPoolEnabled")
1038
+ ] }),
1039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
1040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1041
+ type: "checkbox",
1042
+ checked: preferences.independentEntry === true,
1043
+ onChange: (event) => edit(account.key, {
1044
+ ...preferences,
1045
+ independentEntry: event.target.checked
1046
+ })
1047
+ }),
1048
+ " ",
1049
+ t("accountsIndependent")
1050
+ ] }),
1051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1052
+ style: hint,
1053
+ children: t("accountsIndependentHint")
1054
+ }),
1055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
1056
+ style: {
1057
+ cursor: "pointer",
1058
+ fontSize: 13
1059
+ },
1060
+ children: [
1061
+ t("accountsModels"),
1062
+ " · ",
1063
+ preferences.poolModels === void 0 ? t("accountsModelsAll") : t("accountsModelsCount", { count: preferences.poolModels.length })
1064
+ ]
1065
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1066
+ style: {
1067
+ ...stack,
1068
+ marginTop: 12
1069
+ },
1070
+ children: [
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1072
+ style: hint,
1073
+ children: t("accountsModelsHint")
1074
+ }),
1075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
1076
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1077
+ type: "checkbox",
1078
+ checked: preferences.poolModels === void 0,
1079
+ onChange: (event) => {
1080
+ const next = { ...preferences };
1081
+ if (event.target.checked) delete next.poolModels;
1082
+ else next.poolModels = account.models.map((model) => model.id);
1083
+ edit(account.key, next);
1084
+ }
1085
+ }),
1086
+ " ",
1087
+ t("accountsModelsAutomatic")
1088
+ ] }),
1089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1090
+ style: actions,
1091
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1092
+ type: "button",
1093
+ style: button,
1094
+ onClick: () => edit(account.key, {
1095
+ ...preferences,
1096
+ poolModels: models.map((model) => model.id)
1097
+ }),
1098
+ children: t("modelsSelectAll")
1099
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1100
+ type: "button",
1101
+ style: button,
1102
+ onClick: () => edit(account.key, {
1103
+ ...preferences,
1104
+ poolModels: []
1105
+ }),
1106
+ children: t("modelsSelectNone")
1107
+ })]
1108
+ }),
1109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1110
+ style: {
1111
+ ...stack,
1112
+ gap: 8,
1113
+ maxHeight: 240,
1114
+ overflowY: "auto"
1115
+ },
1116
+ children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1117
+ style: {
1118
+ fontSize: 13,
1119
+ overflowWrap: "anywhere"
1120
+ },
1121
+ children: [
1122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1123
+ type: "checkbox",
1124
+ checked: selected.has(model.id),
1125
+ onChange: (event) => {
1126
+ const next = new Set(selected);
1127
+ if (event.target.checked) next.add(model.id);
1128
+ else next.delete(model.id);
1129
+ edit(account.key, {
1130
+ ...preferences,
1131
+ poolModels: [...next]
1132
+ });
1133
+ }
1134
+ }),
1135
+ " ",
1136
+ model.name,
1137
+ model.unavailable && ` (${t("modelsUnavailable")})`
1138
+ ]
1139
+ }, model.id)), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1140
+ style: hint,
1141
+ children: t("accountsNoModels")
1142
+ })]
1143
+ })
1144
+ ]
1145
+ })] })
1146
+ ]
1147
+ }, account.key);
1148
+ })
1149
+ ]
1150
+ }),
1151
+ (provider !== "cursor-subscription" || (catalog?.accounts.length ?? 0) > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderModelEditor, {
1152
+ ref: editor,
1153
+ provider,
1154
+ rpc,
1155
+ t,
1156
+ disabled: saving,
1157
+ onDirtyChange: setModelsDirty
1158
+ }),
1159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1160
+ style: {
1161
+ ...stack,
1162
+ gap: 8,
1163
+ borderTop: border,
1164
+ padding: "14px 0 0",
1165
+ position: "sticky",
1166
+ bottom: 0,
1167
+ background: "var(--dsw-alias-bg-layer-1)"
1168
+ },
1169
+ children: [saveError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1170
+ role: "alert",
1171
+ style: {
1172
+ ...hint,
1173
+ color: "var(--dsw-alias-state-error-primary)"
1174
+ },
1175
+ children: saveError
1176
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1177
+ style: {
1178
+ ...actions,
1179
+ justifyContent: "flex-end"
1180
+ },
1181
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1182
+ type: "button",
1183
+ style: button,
1184
+ disabled: saving,
1185
+ onClick: onClose,
1186
+ children: t("cancel")
1187
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1188
+ type: "button",
1189
+ style: {
1190
+ ...button,
1191
+ fontWeight: 600
1192
+ },
1193
+ disabled: loading || saving || !Object.keys(changes).length && !modelsDirty,
1194
+ onClick: () => {
1195
+ save();
1196
+ },
1197
+ children: saving ? t("modelDefaultsSaving") : t("modelsSave")
1198
+ })]
1199
+ })]
1200
+ })
1201
+ ]
1202
+ })
1203
+ });
1204
+ }
1205
+
1206
+ //#endregion
1207
+ //#region src/client/subscription-card-styles.ts
1208
+ /** Shared visual language for OAuth, Cursor, and built-in provider cards. */
1209
+ const subscriptionCardStyles = {
1210
+ card: {
1211
+ border: "1px solid var(--dsw-alias-border-l2)",
1212
+ borderRadius: 12,
1213
+ padding: "12px 14px",
1214
+ display: "flex",
1215
+ flexDirection: "column",
1216
+ gap: 6,
1217
+ color: "var(--dsw-alias-label-primary)"
1218
+ },
1219
+ header: {
1220
+ display: "flex",
1221
+ alignItems: "center",
1222
+ gap: 8
1223
+ },
1224
+ dot: {
1225
+ width: 8,
1226
+ height: 8,
1227
+ borderRadius: "50%",
1228
+ flexShrink: 0
1229
+ },
1230
+ name: {
1231
+ fontWeight: 500,
1232
+ fontSize: 14,
1233
+ lineHeight: "22px",
1234
+ color: "var(--dsw-alias-label-primary)"
1235
+ },
1236
+ status: {
1237
+ margin: 0,
1238
+ fontSize: 12,
1239
+ lineHeight: "18px",
1240
+ color: "var(--dsw-alias-label-tertiary)"
1241
+ },
1242
+ error: {
1243
+ margin: 0,
1244
+ fontSize: 12,
1245
+ lineHeight: "18px",
1246
+ color: "var(--dsw-alias-state-error-primary)"
1247
+ },
1248
+ actions: {
1249
+ display: "flex",
1250
+ gap: 8,
1251
+ marginTop: 4,
1252
+ alignItems: "center",
1253
+ flexWrap: "wrap"
1254
+ },
1255
+ button: {
1256
+ boxSizing: "border-box",
1257
+ display: "inline-flex",
1258
+ alignItems: "center",
1259
+ justifyContent: "center",
1260
+ height: 28,
1261
+ padding: "0 10px",
1262
+ borderRadius: 14,
1263
+ border: "1px solid var(--dsw-alias-border-l2)",
1264
+ background: "transparent",
1265
+ color: "var(--dsw-alias-label-primary)",
1266
+ font: "inherit",
1267
+ fontSize: 12,
1268
+ lineHeight: "18px",
1269
+ cursor: "pointer"
1270
+ },
1271
+ account: {
1272
+ display: "flex",
1273
+ flexDirection: "column",
1274
+ gap: 6,
1275
+ border: "1px solid var(--dsw-alias-border-l2)",
1276
+ borderRadius: 8,
1277
+ padding: "8px 10px",
1278
+ marginTop: 4
1279
+ },
1280
+ accountHeader: {
1281
+ display: "flex",
1282
+ alignItems: "center",
1283
+ gap: 8
1284
+ },
1285
+ accountName: {
1286
+ fontSize: 13,
1287
+ lineHeight: "20px",
1288
+ color: "var(--dsw-alias-label-primary)",
1289
+ userSelect: "all"
1290
+ },
1291
+ defaultStar: { color: "var(--dsw-alias-state-warn-label)" },
1292
+ usage: {
1293
+ display: "flex",
1294
+ flexDirection: "column",
1295
+ gap: 6,
1296
+ marginTop: 4,
1297
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
1298
+ paddingTop: 8
1299
+ },
1300
+ usageHeader: {
1301
+ display: "flex",
1302
+ alignItems: "center",
1303
+ gap: 8
1304
+ },
1305
+ usageTitle: {
1306
+ fontSize: 12,
1307
+ lineHeight: "18px",
1308
+ fontWeight: 500,
1309
+ color: "var(--dsw-alias-label-secondary)"
1310
+ },
1311
+ usagePlan: {
1312
+ fontSize: 12,
1313
+ lineHeight: "18px",
1314
+ color: "var(--dsw-alias-label-tertiary)"
1315
+ },
1316
+ usageRefresh: {
1317
+ boxSizing: "border-box",
1318
+ display: "inline-flex",
1319
+ alignItems: "center",
1320
+ justifyContent: "center",
1321
+ height: 22,
1322
+ padding: "0 8px",
1323
+ borderRadius: 11,
1324
+ marginLeft: "auto",
1325
+ border: "1px solid var(--dsw-alias-border-l2)",
1326
+ background: "transparent",
1327
+ color: "var(--dsw-alias-label-secondary)",
1328
+ font: "inherit",
1329
+ fontSize: 12,
1330
+ lineHeight: "18px",
1331
+ cursor: "pointer"
1332
+ },
1333
+ usageRow: {
1334
+ display: "flex",
1335
+ flexDirection: "column",
1336
+ gap: 3
1337
+ },
1338
+ usageMeta: {
1339
+ display: "flex",
1340
+ justifyContent: "space-between",
1341
+ gap: 8,
1342
+ fontSize: 12,
1343
+ lineHeight: "18px",
1344
+ color: "var(--dsw-alias-label-tertiary)"
1345
+ },
1346
+ usageTrack: {
1347
+ height: 6,
1348
+ borderRadius: 3,
1349
+ overflow: "hidden",
1350
+ background: "var(--dsw-alias-bg-layer-1)",
1351
+ border: "1px solid var(--dsw-alias-border-l2)"
1352
+ },
1353
+ usageFill: {
1354
+ height: "100%",
1355
+ borderRadius: 3
1356
+ }
1357
+ };
1358
+
1359
+ //#endregion
1360
+ //#region src/client/usage-badge-preferences.ts
1361
+ const MODE_KEY = "dsh.subscriptions.usageBadgeMode";
1362
+ const MODE_EVENT = "dsh:subscriptions:usage-badge-mode";
1363
+ const USAGE_BADGE_REFRESH_EVENT = "dsh:subscriptions:usage-refresh";
1364
+ /** Read the browser preference; blocked storage retains the default display. */
1365
+ function readUsageBadgeMode() {
1366
+ if (typeof window === "undefined") return "recent";
1367
+ try {
1368
+ return window.localStorage.getItem(MODE_KEY) === "hidden" ? "hidden" : "recent";
1369
+ } catch (error) {
1370
+ if (!(error instanceof DOMException) || error.name !== "SecurityError") throw error;
1371
+ return "recent";
1372
+ }
1373
+ }
1374
+ /** Save first, then notify; the settings control reports a rejected write. */
1375
+ function setUsageBadgeMode(mode) {
1376
+ if (mode !== "recent" && mode !== "hidden") throw new TypeError("Invalid subscription usage display mode");
1377
+ window.localStorage.setItem(MODE_KEY, mode);
1378
+ window.dispatchEvent(new Event(MODE_EVENT));
1379
+ }
1380
+ /** Same-tab writes need a custom event; other tabs use the storage event. */
1381
+ function subscribeUsageBadgeMode(notify) {
1382
+ const onStorage = (event) => {
1383
+ if (event.key === MODE_KEY || event.key === null) notify();
1384
+ };
1385
+ window.addEventListener(MODE_EVENT, notify);
1386
+ window.addEventListener("storage", onStorage);
1387
+ return () => {
1388
+ window.removeEventListener(MODE_EVENT, notify);
1389
+ window.removeEventListener("storage", onStorage);
1390
+ };
1391
+ }
1392
+ /** Shared React snapshot used by the settings control and every mounted badge. */
1393
+ function useUsageBadgeMode() {
1394
+ return (0, react.useSyncExternalStore)(subscribeUsageBadgeMode, readUsageBadgeMode, () => "recent");
1395
+ }
1396
+
1397
+ //#endregion
1398
+ //#region src/client/ExternalUsageCards.tsx
1399
+ const SOURCES = [{
1400
+ id: "opencode-go",
1401
+ name: "OpenCode Go",
1402
+ ref: "OPENCODE_GO_API_KEY"
1403
+ }, {
1404
+ id: "kimi-code",
1405
+ name: "Kimi Code",
1406
+ ref: "KIMI_CODING_API_KEY"
1407
+ }];
1408
+ function errorText(error) {
1409
+ return error instanceof Error ? error.message : String(error);
1410
+ }
1411
+ function barColor$1(percent) {
1412
+ if (percent >= 95) return "var(--dsw-alias-state-error-primary)";
1413
+ if (percent >= 80) return "var(--dsw-alias-state-warn-label)";
1414
+ return "var(--dsw-alias-state-success-primary)";
1415
+ }
1416
+ /** Quota cards for API-key providers already available through DSH itself. */
1417
+ function ExternalUsageCards({ rpc, t }) {
1418
+ const [status, setStatus] = (0, react.useState)();
1419
+ const [statusError, setStatusError] = (0, react.useState)();
1420
+ const [usage, setUsage] = (0, react.useState)({});
1421
+ const [errors, setErrors] = (0, react.useState)({});
1422
+ const [loading, setLoading] = (0, react.useState)({});
1423
+ const refresh = (0, react.useCallback)(async (source) => {
1424
+ setLoading((prev) => ({
1425
+ ...prev,
1426
+ [source]: true
1427
+ }));
1428
+ setErrors((prev) => ({
1429
+ ...prev,
1430
+ [source]: void 0
1431
+ }));
1432
+ try {
1433
+ const value = await callSubscriptionsAuth(rpc, "externalUsage", { source });
1434
+ setUsage((prev) => ({
1435
+ ...prev,
1436
+ [source]: value
1437
+ }));
1438
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1439
+ } catch (error) {
1440
+ setErrors((prev) => ({
1441
+ ...prev,
1442
+ [source]: errorText(error)
1443
+ }));
1444
+ } finally {
1445
+ setLoading((prev) => ({
1446
+ ...prev,
1447
+ [source]: false
1448
+ }));
1449
+ }
1450
+ }, [rpc]);
1451
+ const refreshStatus = (0, react.useCallback)(async () => {
1452
+ try {
1453
+ const value = await callSubscriptionsAuth(rpc, "externalStatus", {});
1454
+ setStatus(value);
1455
+ setStatusError(void 0);
1456
+ for (const { id } of SOURCES) if (value[id]?.configured) refresh(id);
1457
+ } catch (error) {
1458
+ setStatusError(errorText(error));
1459
+ }
1460
+ }, [rpc, refresh]);
1461
+ (0, react.useEffect)(() => {
1462
+ refreshStatus();
1463
+ }, [refreshStatus]);
1464
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: SOURCES.map(({ id, name, ref }) => {
1465
+ const configured = status?.[id]?.configured === true;
1466
+ const snapshot = usage[id];
1467
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1468
+ style: subscriptionCardStyles.card,
1469
+ children: [
1470
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1471
+ style: subscriptionCardStyles.header,
1472
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1473
+ ...subscriptionCardStyles.dot,
1474
+ background: configured ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
1475
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1476
+ style: subscriptionCardStyles.name,
1477
+ children: name
1478
+ })]
1479
+ }),
1480
+ !(status === void 0 && statusError !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1481
+ style: subscriptionCardStyles.status,
1482
+ children: status === void 0 ? t("checking") : configured ? t("externalUsageConnected") : t("externalUsageNotConfigured")
1483
+ }),
1484
+ statusError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1485
+ style: subscriptionCardStyles.error,
1486
+ children: t("externalUsageUnavailable", { message: statusError })
1487
+ }),
1488
+ status !== void 0 && !configured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1489
+ style: subscriptionCardStyles.status,
1490
+ children: t("externalUsageConfigure", { ref })
1491
+ }),
1492
+ configured && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1493
+ style: subscriptionCardStyles.account,
1494
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1495
+ style: subscriptionCardStyles.accountHeader,
1496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1497
+ style: subscriptionCardStyles.defaultStar,
1498
+ title: t("defaultBadge"),
1499
+ children: "★"
1500
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1501
+ style: subscriptionCardStyles.accountName,
1502
+ children: t("externalUsageApiKey")
1503
+ })]
1504
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1505
+ style: subscriptionCardStyles.usage,
1506
+ children: [
1507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1508
+ style: subscriptionCardStyles.usageHeader,
1509
+ children: [
1510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1511
+ style: subscriptionCardStyles.usageTitle,
1512
+ children: t("usageTitle")
1513
+ }),
1514
+ snapshot?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1515
+ style: subscriptionCardStyles.usagePlan,
1516
+ children: t("usagePlan", { plan: snapshot.plan })
1517
+ }),
1518
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1519
+ type: "button",
1520
+ style: subscriptionCardStyles.usageRefresh,
1521
+ disabled: loading[id] === true,
1522
+ onClick: () => {
1523
+ refresh(id);
1524
+ },
1525
+ children: t("usageRefresh")
1526
+ })
1527
+ ]
1528
+ }),
1529
+ loading[id] === true && snapshot === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1530
+ style: subscriptionCardStyles.status,
1531
+ children: t("usageLoading")
1532
+ }),
1533
+ errors[id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1534
+ style: subscriptionCardStyles.error,
1535
+ children: t("usageError", { message: errors[id] })
1536
+ }),
1537
+ snapshot?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1538
+ style: subscriptionCardStyles.status,
1539
+ children: t("usageEmpty")
1540
+ }),
1541
+ snapshot?.windows?.map((window$1, index) => {
1542
+ const percent = Math.min(100, Math.max(0, window$1.usedPercent));
1543
+ const label = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : window$1.scope === "Monthly" ? t("usageMonthly") : window$1.scope ?? t("usageWindow");
1544
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
+ style: subscriptionCardStyles.usageRow,
1546
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
+ style: subscriptionCardStyles.usageMeta,
1548
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1549
+ Math.round(percent),
1550
+ "%",
1551
+ window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`
1552
+ ] })]
1553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1554
+ style: subscriptionCardStyles.usageTrack,
1555
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1556
+ ...subscriptionCardStyles.usageFill,
1557
+ width: `${percent}%`,
1558
+ background: barColor$1(percent)
1559
+ } })
1560
+ })]
1561
+ }, index);
1562
+ })
1563
+ ]
1564
+ })]
1565
+ }),
1566
+ !configured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1567
+ style: subscriptionCardStyles.actions,
1568
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1569
+ type: "button",
1570
+ style: subscriptionCardStyles.button,
1571
+ onClick: () => {
1572
+ refreshStatus();
1573
+ },
1574
+ children: t("usageRefresh")
1575
+ })
1576
+ })
1577
+ ]
1578
+ }, id);
1579
+ }) });
1580
+ }
1581
+
1582
+ //#endregion
1583
+ //#region src/client/CursorCard.tsx
1584
+ function message(error) {
1585
+ return error instanceof Error ? error.message : String(error);
1586
+ }
1587
+ function barColor(percent) {
1588
+ if (percent >= 95) return "var(--dsw-alias-state-error-primary)";
1589
+ if (percent >= 80) return "var(--dsw-alias-state-warn-label)";
1590
+ return "var(--dsw-alias-state-success-primary)";
1591
+ }
1592
+ /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
1593
+ function CursorCard({ rpc, t }) {
1594
+ const [status, setStatus] = (0, react.useState)();
1595
+ const [usage, setUsage] = (0, react.useState)();
1596
+ const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)();
1597
+ const [error, setError] = (0, react.useState)();
1598
+ const [loading, setLoading] = (0, react.useState)(false);
1599
+ const [manage, setManage] = (0, react.useState)(false);
1600
+ const authenticated = (0, react.useRef)();
1601
+ const refreshStatus = (0, react.useCallback)(async () => {
1602
+ try {
1603
+ const value = await callSubscriptionsAuth(rpc, "cursorStatus", {});
1604
+ setStatus(value);
1605
+ if (authenticated.current !== value.authenticated) {
1606
+ authenticated.current = value.authenticated;
1607
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1608
+ }
1609
+ if (value.authenticated) setAuthorizeUrl(void 0);
1610
+ if (value.error) setError(value.error);
1611
+ } catch (failure) {
1612
+ setError(message(failure));
1613
+ }
1614
+ }, [rpc]);
1615
+ const refreshUsage = (0, react.useCallback)(async () => {
1616
+ setLoading(true);
1617
+ try {
1618
+ setUsage(await callSubscriptionsAuth(rpc, "cursorUsage", {}));
1619
+ setError(void 0);
1620
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1621
+ } catch (failure) {
1622
+ setError(message(failure));
1623
+ } finally {
1624
+ setLoading(false);
1625
+ }
1626
+ }, [rpc]);
1627
+ (0, react.useEffect)(() => {
1628
+ refreshStatus();
1629
+ }, [refreshStatus]);
1630
+ (0, react.useEffect)(() => {
1631
+ if (status?.busy !== true) return;
1632
+ const timer = setInterval(() => {
1633
+ refreshStatus();
1634
+ }, 2e3);
1635
+ return () => clearInterval(timer);
1636
+ }, [status?.busy, refreshStatus]);
1637
+ (0, react.useEffect)(() => {
1638
+ if (status?.authenticated === true) refreshUsage();
1639
+ }, [status?.authenticated, refreshUsage]);
1640
+ const login = async () => {
1641
+ setError(void 0);
1642
+ try {
1643
+ const value = await callSubscriptionsAuth(rpc, "cursorLogin", {});
1644
+ setAuthorizeUrl(value.authorizeUrl);
1645
+ window.open(value.authorizeUrl, "_blank", "noopener");
1646
+ await refreshStatus();
1647
+ } catch (failure) {
1648
+ setError(message(failure));
1649
+ }
1650
+ };
1651
+ const cancel = async () => {
1652
+ try {
1653
+ await callSubscriptionsAuth(rpc, "cursorCancel", {});
1654
+ setAuthorizeUrl(void 0);
1655
+ await refreshStatus();
1656
+ } catch (failure) {
1657
+ setError(message(failure));
1658
+ }
1659
+ };
1660
+ const logout = async () => {
1661
+ if (!window.confirm(t("logoutAccountConfirm", {
1662
+ account: "Cursor",
1663
+ provider: "Cursor"
1664
+ }))) return;
1665
+ try {
1666
+ await callSubscriptionsAuth(rpc, "cursorLogout", {});
1667
+ setUsage(void 0);
1668
+ await refreshStatus();
1669
+ } catch (failure) {
1670
+ setError(message(failure));
1671
+ }
1672
+ };
1673
+ const connected = status?.authenticated === true;
1674
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1675
+ style: subscriptionCardStyles.card,
1676
+ children: [
1677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1678
+ style: subscriptionCardStyles.header,
1679
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1680
+ ...subscriptionCardStyles.dot,
1681
+ background: status?.busy ? "var(--dsw-alias-state-warn-label)" : connected ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
1682
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1683
+ style: subscriptionCardStyles.name,
1684
+ children: t("cursorTitle")
1685
+ })]
1686
+ }),
1687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1688
+ style: subscriptionCardStyles.status,
1689
+ children: status === void 0 ? t("checking") : status.busy ? t("loginInProgress") : connected ? t("loggedInCount", { count: 1 }) : t("notLoggedIn")
1690
+ }),
1691
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1692
+ style: subscriptionCardStyles.error,
1693
+ children: error
1694
+ }),
1695
+ connected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1696
+ style: subscriptionCardStyles.account,
1697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1698
+ style: subscriptionCardStyles.accountHeader,
1699
+ children: [
1700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1701
+ style: subscriptionCardStyles.defaultStar,
1702
+ title: t("defaultBadge"),
1703
+ children: "★"
1704
+ }),
1705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1706
+ style: subscriptionCardStyles.accountName,
1707
+ children: "Cursor"
1708
+ }),
1709
+ status.expiresAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1710
+ style: subscriptionCardStyles.status,
1711
+ children: t("accountExpires", { date: new Date(status.expiresAt).toLocaleString() })
1712
+ }),
1713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1714
+ type: "button",
1715
+ style: {
1716
+ ...subscriptionCardStyles.button,
1717
+ marginLeft: "auto",
1718
+ flexShrink: 0
1719
+ },
1720
+ onClick: () => {
1721
+ logout();
1722
+ },
1723
+ children: t("logout")
1724
+ })
1725
+ ]
1726
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1727
+ style: subscriptionCardStyles.usage,
1728
+ children: [
1729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1730
+ style: subscriptionCardStyles.usageHeader,
1731
+ children: [
1732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1733
+ style: subscriptionCardStyles.usageTitle,
1734
+ children: t("usageTitle")
1735
+ }),
1736
+ usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1737
+ style: subscriptionCardStyles.status,
1738
+ children: t("usagePlan", { plan: usage.plan })
1739
+ }),
1740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1741
+ type: "button",
1742
+ style: subscriptionCardStyles.usageRefresh,
1743
+ disabled: loading,
1744
+ onClick: () => {
1745
+ refreshUsage();
1746
+ },
1747
+ children: t("usageRefresh")
1748
+ })
1749
+ ]
1750
+ }),
1751
+ loading && usage === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1752
+ style: subscriptionCardStyles.status,
1753
+ children: t("usageLoading")
1754
+ }),
1755
+ usage?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1756
+ style: subscriptionCardStyles.status,
1757
+ children: t("usageEmpty")
1758
+ }),
1759
+ usage?.windows?.map((window$1, index) => {
1760
+ const percent = Math.min(100, Math.max(0, window$1.usedPercent));
1761
+ const label = window$1.scope === "Included" ? t("cursorIncluded") : window$1.scope === "Cursor Models" ? t("cursorModels") : window$1.scope === "Other Models" ? t("cursorOtherModels") : window$1.scope === "Included requests" ? t("cursorIncludedRequests") : window$1.scope ?? t("usageWindow");
1762
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1763
+ style: subscriptionCardStyles.usageRow,
1764
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1765
+ style: subscriptionCardStyles.usageMeta,
1766
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1767
+ Math.round(percent),
1768
+ "%",
1769
+ window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`
1770
+ ] })]
1771
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1772
+ style: subscriptionCardStyles.usageTrack,
1773
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1774
+ width: `${percent}%`,
1775
+ height: "100%",
1776
+ borderRadius: 3,
1777
+ background: barColor(percent)
1778
+ } })
1779
+ })]
1780
+ }, index);
1781
+ })
1782
+ ]
1783
+ })]
1784
+ }),
1785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1786
+ style: subscriptionCardStyles.actions,
1787
+ children: [
1788
+ status?.authenticated === false && !status.busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1789
+ type: "button",
1790
+ style: subscriptionCardStyles.button,
1791
+ onClick: () => {
1792
+ login();
1793
+ },
1794
+ children: t("login")
1795
+ }),
1796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1797
+ type: "button",
1798
+ style: subscriptionCardStyles.button,
1799
+ "aria-haspopup": "dialog",
1800
+ onClick: () => setManage(true),
1801
+ children: t("accountsManage")
1802
+ }),
1803
+ status?.busy === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1804
+ type: "button",
1805
+ style: subscriptionCardStyles.button,
1806
+ onClick: () => {
1807
+ cancel();
1808
+ },
1809
+ children: t("cancel")
1810
+ })
1811
+ ]
1812
+ }),
1813
+ status?.busy === true && authorizeUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1814
+ href: authorizeUrl,
1815
+ target: "_blank",
1816
+ rel: "noopener noreferrer",
1817
+ children: t("cursorOpenLogin")
1818
+ }),
1819
+ manage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderAccountManager, {
1820
+ provider: "cursor-subscription",
1821
+ name: "Cursor",
1822
+ rpc,
1823
+ t,
1824
+ onClose: () => setManage(false)
1825
+ })
1826
+ ]
1827
+ });
1828
+ }
1829
+
1830
+ //#endregion
1831
+ //#region src/client/UsageBadgeDisplaySetting.tsx
1832
+ /** Browser-local display preference; never changes provider or account settings. */
1833
+ function UsageBadgeDisplaySetting({ t }) {
1834
+ const mode = useUsageBadgeMode();
1835
+ const [failed, setFailed] = (0, react.useState)(false);
1836
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
+ style: styles$6.card,
1838
+ children: [
1839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1840
+ style: styles$6.field,
1841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageBadgeDisplay") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1842
+ style: styles$6.select,
1843
+ "aria-label": t("usageBadgeDisplay"),
1844
+ value: mode,
1845
+ onChange: (event) => {
1846
+ const value = event.currentTarget.value;
1847
+ if (value !== "recent" && value !== "hidden") return;
1848
+ try {
1849
+ setUsageBadgeMode(value);
1850
+ setFailed(false);
1851
+ } catch (error) {
1852
+ if (!(error instanceof DOMException) || !["SecurityError", "QuotaExceededError"].includes(error.name)) throw error;
1853
+ setFailed(true);
1854
+ }
1855
+ },
1856
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1857
+ value: "recent",
1858
+ children: t("usageBadgeDisplayRecent")
1859
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1860
+ value: "hidden",
1861
+ children: t("usageBadgeDisplayHidden")
1862
+ })]
1863
+ })]
1864
+ }),
1865
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1866
+ style: styles$6.hint,
1867
+ children: t("usageBadgeDisplayHint")
1868
+ }),
1869
+ failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1870
+ role: "alert",
1871
+ style: {
1872
+ ...styles$6.hint,
1873
+ color: "var(--dsw-alias-state-error-primary)"
1874
+ },
1875
+ children: t("usageBadgeDisplaySaveFailed")
1876
+ })
1877
+ ]
1878
+ });
1879
+ }
1880
+ const styles$6 = {
1881
+ card: {
1882
+ border: "1px solid var(--dsw-alias-border-l2)",
1883
+ borderRadius: 12,
1884
+ padding: "12px 14px",
1885
+ display: "flex",
1886
+ flexDirection: "column",
1887
+ gap: 6
1888
+ },
1889
+ field: {
1890
+ display: "flex",
1891
+ flexDirection: "column",
1892
+ gap: 4,
1893
+ fontSize: 14,
1894
+ lineHeight: "22px"
1895
+ },
1896
+ select: {
1897
+ height: 32,
1898
+ width: "100%",
1899
+ boxSizing: "border-box",
1900
+ border: "1px solid var(--dsw-alias-border-l2)",
1901
+ borderRadius: 8,
1902
+ padding: "0 10px",
1903
+ font: "inherit",
1904
+ background: "var(--dsw-alias-bg-layer-1)",
1905
+ color: "var(--dsw-alias-label-primary)"
1906
+ },
1907
+ hint: {
1908
+ margin: 0,
1909
+ fontSize: 12,
1910
+ lineHeight: "18px",
1911
+ color: "var(--dsw-alias-label-tertiary)"
1912
+ }
1913
+ };
1914
+
1915
+ //#endregion
1916
+ //#region src/client/SubscriptionsSection.tsx
1917
+ /** Poll cadence while a provider login attempt is busy. */
1918
+ const POLL_INTERVAL_MS$1 = 2e3;
1919
+ const CLIENT_VERSION_SOURCES = {
1920
+ npm: "clientVersionNpm",
1921
+ local: "clientVersionLocal",
1922
+ fallback: "clientVersionFallback",
1923
+ config: "clientVersionConfig"
1924
+ };
1925
+ /** Card display metadata, in page order (names are brand names, not translated). */
1926
+ const PROVIDERS = [
1927
+ {
1928
+ id: "codex",
1929
+ name: "Codex (ChatGPT)"
1930
+ },
1931
+ {
1932
+ id: "claude",
1933
+ name: "Claude"
1934
+ },
1935
+ {
1936
+ id: "grok",
1937
+ name: "Grok (X Premium)"
1938
+ },
1939
+ {
1940
+ id: "copilot",
1941
+ name: "GitHub Copilot"
1942
+ },
1943
+ {
1944
+ id: "antigravity",
1945
+ name: "Google Antigravity"
1946
+ }
1947
+ ];
1948
+ /** Human text of an action failure, SubscriptionsAuthError or not. */
1949
+ function messageOf(error) {
1950
+ return error instanceof Error ? error.message : String(error);
1951
+ }
1952
+ /** Copy a keyed map without the entries whose key is not in `live`. */
1953
+ function dropStale(map, live) {
1954
+ const stale = Object.keys(map).filter((key) => !live.has(key));
1955
+ if (stale.length === 0) return map;
1956
+ const next = { ...map };
1957
+ for (const key of stale) delete next[key];
1958
+ return next;
1959
+ }
1960
+ /**
1961
+ * English-dictionary fallback for a missing inject `t` (standalone renders);
1962
+ * the slot inject always supplies the locale-bound one.
1963
+ * @param key - dictionary key.
1964
+ * @param params - `{name}` template params.
1965
+ * @returns the template with params substituted.
1966
+ */
1967
+ function fallbackTranslate$4(key, params) {
1968
+ let text = en[key];
1969
+ for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
1970
+ return text;
1971
+ }
1972
+ const styles$5 = {
1973
+ section: {
1974
+ display: "flex",
1975
+ flexDirection: "column",
1976
+ gap: 12,
1977
+ maxWidth: 560,
1978
+ color: "var(--dsw-alias-label-primary)"
1979
+ },
1980
+ intro: {
1981
+ margin: 0,
1982
+ color: "var(--dsw-alias-label-tertiary)",
1983
+ fontSize: 14,
1984
+ lineHeight: "22px"
1985
+ },
1986
+ card: subscriptionCardStyles.card,
1987
+ cardHeader: subscriptionCardStyles.header,
1988
+ dot: subscriptionCardStyles.dot,
1989
+ name: subscriptionCardStyles.name,
1990
+ statusLine: subscriptionCardStyles.status,
1991
+ clientVersion: {
1992
+ fontSize: 12,
1993
+ lineHeight: "18px",
1994
+ color: "var(--dsw-alias-label-tertiary)",
1995
+ fontVariantNumeric: "tabular-nums"
1996
+ },
1997
+ errorLine: subscriptionCardStyles.error,
1998
+ actions: subscriptionCardStyles.actions,
1999
+ button: subscriptionCardStyles.button,
2000
+ usage: subscriptionCardStyles.usage,
2001
+ usageHeader: subscriptionCardStyles.usageHeader,
2002
+ usageTitle: subscriptionCardStyles.usageTitle,
2003
+ usagePlan: subscriptionCardStyles.usagePlan,
2004
+ usageRefresh: subscriptionCardStyles.usageRefresh,
2005
+ usageRow: subscriptionCardStyles.usageRow,
2006
+ usageMeta: subscriptionCardStyles.usageMeta,
2007
+ accountRow: subscriptionCardStyles.account,
2008
+ accountHeader: subscriptionCardStyles.accountHeader,
2009
+ accountName: subscriptionCardStyles.accountName,
2010
+ starButton: {
2011
+ border: "none",
2012
+ background: "transparent",
2013
+ padding: 0,
2014
+ font: "inherit",
2015
+ fontSize: 14,
2016
+ lineHeight: "20px",
2017
+ cursor: "pointer",
2018
+ color: "var(--dsw-alias-state-warn-label)"
2019
+ },
2020
+ usageTrack: subscriptionCardStyles.usageTrack,
2021
+ usageFill: subscriptionCardStyles.usageFill,
2022
+ deviceCode: {
2023
+ marginTop: 4,
2024
+ display: "flex",
2025
+ flexDirection: "column",
2026
+ gap: 6,
2027
+ border: "1px solid var(--dsw-alias-border-l2)",
2028
+ borderRadius: 8,
2029
+ padding: "10px 12px",
2030
+ background: "var(--dsw-alias-bg-layer-1)"
2031
+ },
2032
+ deviceCodeText: {
2033
+ fontFamily: "monospace",
2034
+ fontSize: 18,
2035
+ lineHeight: "24px",
2036
+ letterSpacing: 2,
2037
+ color: "var(--dsw-alias-label-primary)",
2038
+ userSelect: "all"
2039
+ },
2040
+ modalOverlay: {
2041
+ position: "fixed",
2042
+ inset: 0,
2043
+ zIndex: 1e3,
2044
+ display: "flex",
2045
+ alignItems: "center",
2046
+ justifyContent: "center",
2047
+ padding: 16,
2048
+ background: "rgba(0, 0, 0, 0.45)"
2049
+ },
2050
+ modal: {
2051
+ width: 460,
2052
+ maxWidth: "100%",
2053
+ maxHeight: "90vh",
2054
+ overflowY: "auto",
2055
+ boxSizing: "border-box",
2056
+ display: "flex",
2057
+ flexDirection: "column",
2058
+ gap: 12,
2059
+ padding: "16px 18px",
2060
+ borderRadius: 12,
2061
+ background: "var(--dsw-alias-bg-layer-1)",
2062
+ border: "1px solid var(--dsw-alias-border-l2)"
2063
+ },
2064
+ modalHeader: {
2065
+ display: "flex",
2066
+ alignItems: "center",
2067
+ gap: 8
2068
+ },
2069
+ modalTitle: {
2070
+ fontWeight: 600,
2071
+ fontSize: 15,
2072
+ lineHeight: "22px",
2073
+ color: "var(--dsw-alias-label-primary)"
2074
+ }
2075
+ };
2076
+ /** Status dot color for one provider state. */
2077
+ function dotColor(status) {
2078
+ if (status?.busy === true) return "var(--dsw-alias-state-warn-label)";
2079
+ if ((status?.accounts.length ?? 0) > 0) return "var(--dsw-alias-state-success-primary)";
2080
+ return "var(--dsw-alias-label-dimmed)";
2081
+ }
2082
+ /**
2083
+ * One-line status text for one provider state.
2084
+ * @param t - section translate.
2085
+ * @param status - the provider's last reported state.
2086
+ * @returns the localized status line.
2087
+ */
2088
+ function statusText(t, status) {
2089
+ if (status === void 0) return t("checking");
2090
+ if (status.busy) return t("loginInProgress");
2091
+ if (status.accounts.length > 0) return t("loggedInCount", { count: status.accounts.length });
2092
+ return t("notLoggedIn");
2093
+ }
2094
+ /**
2095
+ * The CLI version a route presents, beside the provider name; nothing for
2096
+ * routes without one. The source word (npm / local / built-in / configured)
2097
+ * is the fix for upstream issue #108: a failed npm lookup must read as a
2098
+ * fallback, never as a plan limit.
2099
+ */
2100
+ function clientVersionText(t, clientVersion) {
2101
+ if (clientVersion === void 0) return "";
2102
+ return t("clientVersion", {
2103
+ version: clientVersion.version,
2104
+ source: t(CLIENT_VERSION_SOURCES[clientVersion.source])
2105
+ });
2106
+ }
2107
+ /**
2108
+ * Localized label of one usage window (kind, plus the model scope when named).
2109
+ * @param t - section translate.
2110
+ * @param window - the reported window.
2111
+ * @returns e.g. "5-hour window" or "Weekly · Opus".
2112
+ */
2113
+ function usageWindowLabel$1(t, window$1) {
2114
+ const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
2115
+ return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
2116
+ }
2117
+ /** Bar fill color: success normally, warn from 80%, error from 95%. Shared with the composer badge. */
2118
+ function usageBarColor(usedPercent$1) {
2119
+ if (usedPercent$1 >= 95) return "var(--dsw-alias-state-error-primary)";
2120
+ if (usedPercent$1 >= 80) return "var(--dsw-alias-state-warn-label)";
2121
+ return "var(--dsw-alias-state-success-primary)";
2122
+ }
2123
+ /**
2124
+ * The Subscriptions settings page component.
2125
+ * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
2126
+ * @returns the section body, or a notice while the RPC face is absent.
2127
+ */
2128
+ function SubscriptionsSection(props) {
2129
+ const { rpc } = props;
2130
+ const t = props.t ?? fallbackTranslate$4;
2131
+ const [statuses, setStatuses] = (0, react.useState)({});
2132
+ const [errors, setErrors] = (0, react.useState)({});
2133
+ const [manualDrafts, setManualDrafts] = (0, react.useState)({
2134
+ codex: "",
2135
+ claude: "",
2136
+ grok: "",
2137
+ copilot: "",
2138
+ antigravity: ""
2139
+ });
2140
+ /** Pending device-flow codes (copilot), shown while the attempt polls. */
2141
+ const [deviceCodes, setDeviceCodes] = (0, react.useState)({});
2142
+ const [copiedCode, setCopiedCode] = (0, react.useState)(void 0);
2143
+ /** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
2144
+ const [usages, setUsages] = (0, react.useState)({});
2145
+ const [usageErrors, setUsageErrors] = (0, react.useState)({});
2146
+ const [usageLoading, setUsageLoading] = (0, react.useState)({});
2147
+ const mountedRef = (0, react.useRef)(true);
2148
+ const pollersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2149
+ /** Accounts with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
2150
+ const usageInflightRef = (0, react.useRef)(/* @__PURE__ */ new Set());
2151
+ const usageRosterSignatureRef = (0, react.useRef)();
2152
+ const [managedProvider, setManagedProvider] = (0, react.useState)();
2153
+ const setProviderError = (0, react.useCallback)((provider, message$1) => {
2154
+ if (!mountedRef.current) return;
2155
+ setErrors((prev) => {
2156
+ const next = { ...prev };
2157
+ if (message$1 === void 0) delete next[provider];
2158
+ else next[provider] = message$1;
2159
+ return next;
2160
+ });
2161
+ }, []);
2162
+ const stopPolling = (0, react.useCallback)((provider) => {
2163
+ const poller = pollersRef.current.get(provider);
2164
+ if (poller !== void 0) {
2165
+ clearInterval(poller);
2166
+ pollersRef.current.delete(provider);
2167
+ }
2168
+ }, []);
2169
+ /** Refetch every provider's status; stop a provider's poller once its attempt settles. */
2170
+ const refresh = (0, react.useCallback)(async () => {
2171
+ if (rpc === void 0) return;
2172
+ let response;
2173
+ try {
2174
+ response = await callSubscriptionsAuth(rpc, "status", {});
2175
+ } catch (error) {
2176
+ const message$1 = error instanceof Error ? error.message : String(error);
2177
+ for (const { id } of PROVIDERS) setProviderError(id, message$1);
2178
+ return;
2179
+ }
2180
+ if (!mountedRef.current) return;
2181
+ setStatuses(response.providers);
2182
+ const signature = PROVIDERS.map(({ id }) => `${id}:${response.providers[id].accounts.map((account) => `${account.key}:${account.isDefault}`).join(",")}`).join(";");
2183
+ if (signature !== usageRosterSignatureRef.current) {
2184
+ usageRosterSignatureRef.current = signature;
2185
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2186
+ }
2187
+ for (const { id } of PROVIDERS) setProviderError(id, void 0);
2188
+ for (const { id } of PROVIDERS) {
2189
+ const status = response.providers[id];
2190
+ if (status.accounts.length > 0 || !status.busy) {
2191
+ stopPolling(id);
2192
+ setDeviceCodes((prev) => {
2193
+ if (prev[id] === void 0) return prev;
2194
+ const next = { ...prev };
2195
+ delete next[id];
2196
+ return next;
2197
+ });
2198
+ }
2199
+ }
2200
+ }, [
2201
+ rpc,
2202
+ stopPolling,
2203
+ setProviderError
2204
+ ]);
2205
+ const startPolling = (0, react.useCallback)((provider) => {
2206
+ if (pollersRef.current.has(provider)) return;
2207
+ pollersRef.current.set(provider, setInterval(() => {
2208
+ refresh();
2209
+ }, POLL_INTERVAL_MS$1));
2210
+ }, [refresh]);
2211
+ (0, react.useEffect)(() => {
2212
+ mountedRef.current = true;
2213
+ refresh().then(() => {
2214
+ if (!mountedRef.current) return;
2215
+ setStatuses((current) => {
2216
+ for (const { id } of PROVIDERS) if (current[id]?.busy === true) startPolling(id);
2217
+ return current;
2218
+ });
2219
+ });
2220
+ return () => {
2221
+ mountedRef.current = false;
2222
+ for (const poller of pollersRef.current.values()) clearInterval(poller);
2223
+ pollersRef.current.clear();
2224
+ };
2225
+ }, [refresh, startPolling]);
2226
+ const loadUsage = (0, react.useCallback)(async (provider, account, force = false) => {
2227
+ const key = `${provider}:${account}`;
2228
+ if (rpc === void 0 || usageInflightRef.current.has(key)) return;
2229
+ usageInflightRef.current.add(key);
2230
+ setUsageLoading((prev) => ({
2231
+ ...prev,
2232
+ [key]: true
2233
+ }));
2234
+ try {
2235
+ const usage = await callSubscriptionsAuth(rpc, "usage", {
2236
+ provider,
2237
+ account,
2238
+ ...force ? { force: true } : {}
2239
+ });
2240
+ if (!mountedRef.current) return;
2241
+ setUsages((prev) => ({
2242
+ ...prev,
2243
+ [key]: usage
2244
+ }));
2245
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2246
+ setUsageErrors((prev) => {
2247
+ const next = { ...prev };
2248
+ delete next[key];
2249
+ return next;
2250
+ });
2251
+ } catch (error) {
2252
+ if (mountedRef.current) setUsageErrors((prev) => ({
2253
+ ...prev,
2254
+ [key]: messageOf(error)
2255
+ }));
2256
+ } finally {
2257
+ usageInflightRef.current.delete(key);
2258
+ if (mountedRef.current) setUsageLoading((prev) => ({
2259
+ ...prev,
2260
+ [key]: false
2261
+ }));
2262
+ }
2263
+ }, [rpc]);
2264
+ (0, react.useEffect)(() => {
2265
+ const live = /* @__PURE__ */ new Set();
2266
+ for (const { id } of PROVIDERS) for (const account of statuses[id]?.accounts ?? []) {
2267
+ const key = `${id}:${account.key}`;
2268
+ live.add(key);
2269
+ if (usages[key] === void 0 && usageErrors[key] === void 0) loadUsage(id, account.key);
2270
+ }
2271
+ setUsages((prev) => dropStale(prev, live));
2272
+ setUsageErrors((prev) => dropStale(prev, live));
2273
+ }, [
2274
+ statuses,
2275
+ usages,
2276
+ usageErrors,
2277
+ loadUsage
2278
+ ]);
2279
+ const login = (0, react.useCallback)(async (provider, method) => {
2280
+ if (rpc === void 0) return;
2281
+ setProviderError(provider, void 0);
2282
+ try {
2283
+ const response = await callSubscriptionsAuth(rpc, "login", {
2284
+ provider,
2285
+ ...method === void 0 ? {} : { method }
2286
+ });
2287
+ if (typeof response.authorizeUrl === "string" && response.authorizeUrl === "") {
2288
+ await refresh();
2289
+ return;
2290
+ }
2291
+ if (typeof response.authorizeUrl !== "string") throw new SubscriptionsAuthError(t("loginMissingUrl"));
2292
+ if (!mountedRef.current) return;
2293
+ setStatuses((prev) => ({
2294
+ ...prev,
2295
+ [provider]: {
2296
+ accounts: prev[provider]?.accounts ?? [],
2297
+ ...prev[provider],
2298
+ busy: true
2299
+ }
2300
+ }));
2301
+ if (typeof response.userCode === "string" && response.userCode.length > 0) setDeviceCodes((prev) => ({
2302
+ ...prev,
2303
+ [provider]: {
2304
+ userCode: response.userCode,
2305
+ verificationUrl: response.authorizeUrl
2306
+ }
2307
+ }));
2308
+ else window.open(response.authorizeUrl, "_blank", "noopener");
2309
+ startPolling(provider);
2310
+ } catch (error) {
2311
+ setProviderError(provider, messageOf(error));
2312
+ }
2313
+ }, [
2314
+ rpc,
2315
+ t,
2316
+ setProviderError,
2317
+ startPolling
2318
+ ]);
2319
+ const cancel = (0, react.useCallback)(async (provider) => {
2320
+ if (rpc === void 0) return;
2321
+ stopPolling(provider);
2322
+ try {
2323
+ await callSubscriptionsAuth(rpc, "cancel", { provider });
2324
+ } catch (error) {
2325
+ setProviderError(provider, messageOf(error));
2326
+ }
2327
+ await refresh();
2328
+ }, [
2329
+ rpc,
2330
+ stopPolling,
2331
+ setProviderError,
2332
+ refresh
2333
+ ]);
2334
+ const submitManual = (0, react.useCallback)(async (provider) => {
2335
+ if (rpc === void 0) return;
2336
+ const input = manualDrafts[provider].trim();
2337
+ if (input === "") return;
2338
+ setProviderError(provider, void 0);
2339
+ try {
2340
+ await callSubscriptionsAuth(rpc, "manual", {
2341
+ provider,
2342
+ input
2343
+ });
2344
+ if (mountedRef.current) setManualDrafts((prev) => ({
2345
+ ...prev,
2346
+ [provider]: ""
2347
+ }));
2348
+ } catch (error) {
2349
+ setProviderError(provider, messageOf(error));
2350
+ }
2351
+ await refresh();
2352
+ }, [
2353
+ rpc,
2354
+ manualDrafts,
2355
+ setProviderError,
2356
+ refresh
2357
+ ]);
2358
+ const logout = (0, react.useCallback)(async (provider, account, display, name) => {
2359
+ if (rpc === void 0) return;
2360
+ if (!window.confirm(t("logoutAccountConfirm", {
2361
+ provider: name,
2362
+ account: display
2363
+ }))) return;
2364
+ setProviderError(provider, void 0);
2365
+ try {
2366
+ await callSubscriptionsAuth(rpc, "logout", {
2367
+ provider,
2368
+ account
2369
+ });
2370
+ } catch (error) {
2371
+ setProviderError(provider, messageOf(error));
2372
+ }
2373
+ await refresh();
2374
+ }, [
2375
+ rpc,
2376
+ t,
2377
+ setProviderError,
2378
+ refresh
2379
+ ]);
2380
+ const setDefault = (0, react.useCallback)(async (provider, account) => {
2381
+ if (rpc === void 0) return;
2382
+ setProviderError(provider, void 0);
2383
+ try {
2384
+ await callSubscriptionsAuth(rpc, "setDefault", {
2385
+ provider,
2386
+ account
2387
+ });
2388
+ } catch (error) {
2389
+ setProviderError(provider, messageOf(error));
2390
+ }
2391
+ await refresh();
2392
+ }, [
2393
+ rpc,
2394
+ setProviderError,
2395
+ refresh
2396
+ ]);
2397
+ const copyDeviceCode = (0, react.useCallback)((provider, userCode) => {
2398
+ navigator.clipboard?.writeText(userCode).then(() => {
2399
+ if (!mountedRef.current) return;
2400
+ setCopiedCode(provider);
2401
+ setTimeout(() => {
2402
+ if (mountedRef.current) setCopiedCode((current) => current === provider ? void 0 : current);
2403
+ }, 1500);
2404
+ }).catch(() => void 0);
2405
+ }, []);
2406
+ if (rpc === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2407
+ style: styles$5.intro,
2408
+ children: t("unavailable")
2409
+ });
2410
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2411
+ style: styles$5.section,
2412
+ children: [
2413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2414
+ style: styles$5.intro,
2415
+ children: t("intro")
2416
+ }),
2417
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBadgeDisplaySetting, { t }),
2418
+ PROVIDERS.map(({ id, name }) => {
2419
+ const status = statuses[id];
2420
+ const busy = status?.busy === true;
2421
+ const deviceCode = deviceCodes[id];
2422
+ const accounts = status?.accounts ?? [];
2423
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2424
+ style: styles$5.card,
2425
+ children: [
2426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2427
+ style: styles$5.cardHeader,
2428
+ children: [
2429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
2430
+ ...styles$5.dot,
2431
+ background: dotColor(status)
2432
+ } }),
2433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2434
+ style: styles$5.name,
2435
+ children: name
2436
+ }),
2437
+ status?.clientVersion !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2438
+ style: styles$5.clientVersion,
2439
+ title: t("clientVersionHint"),
2440
+ children: clientVersionText(t, status.clientVersion)
2441
+ })
2442
+ ]
2443
+ }),
2444
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2445
+ style: styles$5.statusLine,
2446
+ children: statusText(t, status)
2447
+ }),
2448
+ status?.detail !== void 0 && status.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2449
+ style: styles$5.statusLine,
2450
+ children: status.detail
2451
+ }),
2452
+ errors[id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2453
+ style: styles$5.errorLine,
2454
+ children: errors[id]
2455
+ }),
2456
+ accounts.map((account) => {
2457
+ const usageKey = `${id}:${account.key}`;
2458
+ const usage = usages[usageKey];
2459
+ const usageError = usageErrors[usageKey];
2460
+ const display = account.account ?? account.key;
2461
+ const showUsage = usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[usageKey] === true);
2462
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2463
+ style: styles$5.accountRow,
2464
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2465
+ style: styles$5.accountHeader,
2466
+ children: [
2467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2468
+ type: "button",
2469
+ style: styles$5.starButton,
2470
+ title: account.isDefault ? t("defaultBadge") : t("setDefault"),
2471
+ onClick: () => {
2472
+ if (!account.isDefault) setDefault(id, account.key);
2473
+ },
2474
+ children: account.isDefault ? "★" : "☆"
2475
+ }),
2476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2477
+ style: styles$5.accountName,
2478
+ children: display
2479
+ }),
2480
+ account.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2481
+ style: styles$5.usagePlan,
2482
+ children: account.plan
2483
+ }),
2484
+ account.expiresAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2485
+ style: styles$5.statusLine,
2486
+ children: t("accountExpires", { date: new Date(account.expiresAt).toLocaleString() })
2487
+ }),
2488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2489
+ type: "button",
2490
+ style: {
2491
+ ...styles$5.button,
2492
+ marginLeft: "auto",
2493
+ flexShrink: 0
2494
+ },
2495
+ onClick: () => {
2496
+ logout(id, account.key, display, name);
2497
+ },
2498
+ children: t("logout")
2499
+ })
2500
+ ]
2501
+ }), showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2502
+ style: styles$5.usage,
2503
+ children: [
2504
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2505
+ style: styles$5.usageHeader,
2506
+ children: [
2507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2508
+ style: styles$5.usageTitle,
2509
+ children: t("usageTitle")
2510
+ }),
2511
+ usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2512
+ style: styles$5.usagePlan,
2513
+ children: t("usagePlan", { plan: usage.plan })
2514
+ }),
2515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2516
+ type: "button",
2517
+ style: {
2518
+ ...styles$5.usageRefresh,
2519
+ ...usageLoading[usageKey] === true ? {
2520
+ opacity: .5,
2521
+ cursor: "default"
2522
+ } : {}
2523
+ },
2524
+ disabled: usageLoading[usageKey] === true,
2525
+ onClick: () => {
2526
+ loadUsage(id, account.key, true);
2527
+ },
2528
+ children: t("usageRefresh")
2529
+ })
2530
+ ]
2531
+ }),
2532
+ usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2533
+ style: styles$5.statusLine,
2534
+ children: t("usageLoading")
2535
+ }),
2536
+ usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2537
+ style: styles$5.errorLine,
2538
+ children: t("usageError", { message: usageError })
2539
+ }),
2540
+ usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2541
+ style: styles$5.statusLine,
2542
+ children: t("usageEmpty")
2543
+ }),
2544
+ (usage?.windows ?? []).map((window$1, index) => {
2545
+ const percent = Math.min(100, Math.max(0, window$1.usedPercent));
2546
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2547
+ style: styles$5.usageRow,
2548
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2549
+ style: styles$5.usageMeta,
2550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel$1(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${String(Math.round(percent))}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2551
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
+ style: styles$5.usageTrack,
2553
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
2554
+ ...styles$5.usageFill,
2555
+ width: `${String(percent)}%`,
2556
+ background: usageBarColor(percent)
2557
+ } })
2558
+ })]
2559
+ }, index);
2560
+ })
2561
+ ]
2562
+ })]
2563
+ }, account.key);
2564
+ }),
2565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2566
+ style: styles$5.actions,
2567
+ children: [
2568
+ !busy && accounts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2569
+ type: "button",
2570
+ style: styles$5.button,
2571
+ onClick: () => {
2572
+ login(id);
2573
+ },
2574
+ children: t("login")
2575
+ }),
2576
+ !busy && accounts.length > 0 && id === "claude" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2577
+ type: "button",
2578
+ style: styles$5.button,
2579
+ onClick: () => {
2580
+ login(id, "oauth");
2581
+ },
2582
+ children: t("addAccountOAuth")
2583
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2584
+ type: "button",
2585
+ style: styles$5.button,
2586
+ onClick: () => {
2587
+ login(id, "keychain");
2588
+ },
2589
+ children: t("addAccountKeychain")
2590
+ })] }),
2591
+ !busy && accounts.length > 0 && id !== "claude" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2592
+ type: "button",
2593
+ style: styles$5.button,
2594
+ onClick: () => {
2595
+ login(id);
2596
+ },
2597
+ children: t("addAccount")
2598
+ }),
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2600
+ type: "button",
2601
+ style: styles$5.button,
2602
+ "aria-haspopup": "dialog",
2603
+ onClick: () => setManagedProvider({
2604
+ id,
2605
+ name
2606
+ }),
2607
+ children: t("accountsManage")
2608
+ }),
2609
+ busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2610
+ type: "button",
2611
+ style: styles$5.button,
2612
+ onClick: () => {
2613
+ cancel(id);
2614
+ },
2615
+ children: t("cancel")
2616
+ })
2617
+ ]
2618
+ }),
2619
+ !busy && accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2620
+ style: styles$5.statusLine,
2621
+ children: t("addAccountHint")
2622
+ }),
2623
+ busy && deviceCode !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2624
+ style: styles$5.deviceCode,
2625
+ children: [
2626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2627
+ style: styles$5.statusLine,
2628
+ children: t("deviceCodePrompt")
2629
+ }),
2630
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2631
+ style: styles$5.deviceCodeText,
2632
+ children: deviceCode.userCode
2633
+ }),
2634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2635
+ style: styles$5.actions,
2636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2637
+ type: "button",
2638
+ style: styles$5.button,
2639
+ onClick: () => {
2640
+ copyDeviceCode(id, deviceCode.userCode);
2641
+ },
2642
+ children: copiedCode === id ? t("deviceCodeCopied") : t("deviceCodeCopy")
2643
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2644
+ type: "button",
2645
+ style: styles$5.button,
2646
+ onClick: () => {
2647
+ window.open(deviceCode.verificationUrl, "_blank", "noopener");
2648
+ },
2649
+ children: t("deviceCodeOpenPage")
2650
+ })]
2651
+ })
2652
+ ]
2653
+ }),
2654
+ busy && deviceCode === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
2655
+ style: styles$5.manual,
2656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2657
+ style: styles$5.manualRow,
2658
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2659
+ style: styles$5.manualInput,
2660
+ value: manualDrafts[id],
2661
+ placeholder: t("manualPlaceholder"),
2662
+ onChange: (event) => setManualDrafts((prev) => ({
2663
+ ...prev,
2664
+ [id]: event.target.value
2665
+ }))
2666
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2667
+ type: "button",
2668
+ style: styles$5.button,
2669
+ onClick: () => {
2670
+ submitManual(id);
2671
+ },
2672
+ children: t("submit")
2673
+ })]
2674
+ })]
2675
+ })
2676
+ ]
2677
+ }, id);
2678
+ }),
2679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CursorCard, {
2680
+ rpc,
2681
+ t
2682
+ }),
2683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalUsageCards, {
2684
+ rpc,
2685
+ t
2686
+ }),
2687
+ managedProvider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderAccountManager, {
2688
+ provider: managedProvider.id,
2689
+ name: managedProvider.name,
2690
+ rpc,
2691
+ t,
2692
+ onClose: () => setManagedProvider(void 0)
2693
+ })
2694
+ ]
2695
+ });
2696
+ }
2697
+
2698
+ //#endregion
2699
+ //#region src/client/ImageGallery.tsx
2700
+ /** Display box for a lone image (platform rule): long edge 240px with the
2701
+ * rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
2702
+ * `object-fit: cover` — and never upscaled past the image's natural size. The
2703
+ * crop anchor keeps the top of very tall images and the left of very wide
2704
+ * ones, where the informative content usually starts. */
2705
+ function singleFit(attachment) {
2706
+ const natural = attachment.width / attachment.height;
2707
+ const ratio = Math.min(4, Math.max(.25, natural));
2708
+ const box = ratio >= 1 ? {
2709
+ width: 240,
2710
+ height: 240 / ratio
2711
+ } : {
2712
+ width: 240 * ratio,
2713
+ height: 240
2714
+ };
2715
+ const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height);
2716
+ return {
2717
+ width: Math.max(1, Math.round(box.width * scale)),
2718
+ height: Math.max(1, Math.round(box.height * scale)),
2719
+ objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
2720
+ };
2721
+ }
2722
+ const styles$4 = {
2723
+ gallery: {
2724
+ display: "flex",
2725
+ flexWrap: "wrap",
2726
+ gap: 8,
2727
+ justifyContent: "flex-start"
2728
+ },
2729
+ frame: {
2730
+ display: "grid",
2731
+ placeItems: "center",
2732
+ overflow: "hidden",
2733
+ padding: 0,
2734
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
2735
+ borderRadius: 8,
2736
+ background: "var(--dsw-alias-interactive-bg-hover-solid)",
2737
+ cursor: "zoom-in"
2738
+ },
2739
+ tile: {
2740
+ width: 64,
2741
+ height: 64
2742
+ },
2743
+ img: {
2744
+ width: "100%",
2745
+ height: "100%",
2746
+ objectFit: "cover",
2747
+ display: "block"
2748
+ },
2749
+ loading: {
2750
+ fontSize: 12,
2751
+ color: "var(--dsw-alias-label-tertiary)",
2752
+ padding: "0 8px"
2753
+ },
2754
+ error: {
2755
+ fontSize: 12,
2756
+ color: "var(--dsw-alias-state-error-primary)",
2757
+ cursor: "pointer",
2758
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
2759
+ borderRadius: 8,
2760
+ background: "transparent",
2761
+ padding: "6px 10px"
2762
+ },
2763
+ overlay: {
2764
+ position: "fixed",
2765
+ inset: 0,
2766
+ zIndex: 1e3,
2767
+ display: "grid",
2768
+ placeItems: "center",
2769
+ background: "rgba(0, 0, 0, 0.72)",
2770
+ padding: 24
2771
+ },
2772
+ overlayImg: {
2773
+ maxWidth: "92vw",
2774
+ maxHeight: "92vh",
2775
+ objectFit: "contain",
2776
+ borderRadius: 4
2777
+ },
2778
+ close: {
2779
+ position: "absolute",
2780
+ top: 12,
2781
+ right: 12,
2782
+ width: 32,
2783
+ height: 32,
2784
+ display: "grid",
2785
+ placeItems: "center",
2786
+ border: "none",
2787
+ borderRadius: "50%",
2788
+ cursor: "pointer",
2789
+ background: "rgba(255, 255, 255, 0.16)",
2790
+ color: "#fff",
2791
+ fontSize: 16,
2792
+ lineHeight: 1
2793
+ }
2794
+ };
2795
+ /**
2796
+ * Full-viewport original-image preview: backdrop or close-control click and
2797
+ * Escape all dismiss; the image itself is inert so a click on it does not
2798
+ * fall through to the backdrop dismissal.
2799
+ */
2800
+ function ImageLightbox({ src, alt, labels, onClose }) {
2801
+ (0, react.useEffect)(() => {
2802
+ const onKey = (event) => {
2803
+ if (event.key === "Escape") onClose();
2804
+ };
2805
+ window.addEventListener("keydown", onKey);
2806
+ return () => {
2807
+ window.removeEventListener("keydown", onKey);
2808
+ };
2809
+ }, [onClose]);
2810
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2811
+ role: "dialog",
2812
+ "aria-label": labels.dialog,
2813
+ style: styles$4.overlay,
2814
+ onClick: onClose,
2815
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2816
+ src,
2817
+ alt,
2818
+ style: styles$4.overlayImg,
2819
+ onClick: (event) => {
2820
+ event.stopPropagation();
2821
+ }
2822
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2823
+ type: "button",
2824
+ "aria-label": labels.close,
2825
+ style: styles$4.close,
2826
+ onClick: onClose,
2827
+ children: "×"
2828
+ })]
2829
+ });
2830
+ }
2831
+ /**
2832
+ * Compact history renderer with retryable loading and click-to-open original
2833
+ * preview. A lone image renders at its `singleFit` size; an image among
2834
+ * several renders as a fixed 64px square tile.
2835
+ */
2836
+ function MessageImage({ attachment, load, variant, labels }) {
2837
+ const [src, setSrc] = (0, react.useState)(null);
2838
+ const [error, setError] = (0, react.useState)(false);
2839
+ const [open, setOpen] = (0, react.useState)(false);
2840
+ const [attempt, setAttempt] = (0, react.useState)(0);
2841
+ const retry = (0, react.useCallback)(() => {
2842
+ setAttempt((a) => a + 1);
2843
+ }, []);
2844
+ const close = (0, react.useCallback)(() => {
2845
+ setOpen(false);
2846
+ }, []);
2847
+ const fit = (0, react.useMemo)(() => variant === "single" ? singleFit(attachment) : void 0, [attachment, variant]);
2848
+ (0, react.useEffect)(() => {
2849
+ let live = true;
2850
+ setError(false);
2851
+ setSrc(null);
2852
+ load(attachment).then((url) => {
2853
+ if (live) setSrc(url);
2854
+ }).catch(() => {
2855
+ if (live) setError(true);
2856
+ });
2857
+ return () => {
2858
+ live = false;
2859
+ };
2860
+ }, [
2861
+ attachment,
2862
+ load,
2863
+ attempt
2864
+ ]);
2865
+ const label = attachment.name ?? labels.image;
2866
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2867
+ type: "button",
2868
+ style: styles$4.error,
2869
+ onClick: retry,
2870
+ children: labels.loadFailed
2871
+ });
2872
+ const box = fit === void 0 ? styles$4.tile : {
2873
+ width: fit.width,
2874
+ height: fit.height
2875
+ };
2876
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2877
+ type: "button",
2878
+ style: {
2879
+ ...styles$4.frame,
2880
+ ...box
2881
+ },
2882
+ title: labels.open,
2883
+ "aria-label": labels.openNamed(label),
2884
+ onClick: () => {
2885
+ if (src !== null) setOpen(true);
2886
+ },
2887
+ children: src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2888
+ style: styles$4.loading,
2889
+ children: labels.loading
2890
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2891
+ src,
2892
+ alt: label,
2893
+ style: {
2894
+ ...styles$4.img,
2895
+ objectPosition: fit?.objectPosition
2896
+ }
2897
+ })
2898
+ }), open && src !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
2899
+ src,
2900
+ alt: label,
2901
+ labels: labels.lightbox,
2902
+ onClose: close
2903
+ })] });
2904
+ }
2905
+ /** Wrapping image group: a lone image renders large, several render as 64px
2906
+ * square tiles (same rule as the platform gallery). */
2907
+ function ImageGallery({ images, load, labels }) {
2908
+ if (images.length === 0) return null;
2909
+ const variant = images.length === 1 ? "single" : "tile";
2910
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2911
+ style: styles$4.gallery,
2912
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImage, {
2913
+ attachment: image.attachment,
2914
+ load,
2915
+ variant,
2916
+ labels
2917
+ }, `${image.attachment.attachmentId}:${index}`))
2918
+ });
2919
+ }
2920
+
2921
+ //#endregion
2922
+ //#region src/client/host-contract.ts
2923
+ /**
2924
+ * Everything the client half assumes about the DSH host that the compiler
2925
+ * cannot check.
2926
+ *
2927
+ * The client bundle reads host modules at runtime, not at build time. A
2928
+ * renamed icon export compiles against the old type declarations and then
2929
+ * renders `undefined`. A slot outlet the host stops rendering never mounts our
2930
+ * entry, so the page looks normal and the feature is just gone. A removed DOM
2931
+ * marker only breaks layout. None of these fail `tsc`, and a unit test with
2932
+ * a hand-made host cannot see them either.
2933
+ *
2934
+ * So each such assumption is listed here once, next to the host package that
2935
+ * owns it. `scripts/check-host-contract.mjs` downloads every supported DSH
2936
+ * version and checks that each entry appears in that version's published
2937
+ * code. The host E2E reads the same diagnostics entries to catch slot
2938
+ * crashes. The spec test/host-contract.spec.ts scans src/client and fails
2939
+ * when code starts relying on a literal host name that is missing here.
2940
+ *
2941
+ * This module has no imports, so plain Node scripts can import it directly.
2942
+ */
2943
+ /** Prefix of the console warning the client logs when a host lacks a listed name. */
2944
+ const HOST_CONTRACT_MISS = "dsh-subscriptions: host contract miss";
2945
+ const HOST_CONTRACT = {
2946
+ moduleTable: "@deepseek-ai/dsh-web-frontend",
2947
+ exports: { "@deepseek-ai/dsh-client-ui-primitives": ["useAnchoredPosition", "useDismissOnOutsidePointer"] },
2948
+ icons: {
2949
+ package: "@deepseek-ai/dsh-client-ui-primitives",
2950
+ names: ["Sparkle", "DataOutline"]
2951
+ },
2952
+ slots: {
2953
+ "settings.section": "@deepseek-ai/dsh-client-ui-settings-general",
2954
+ "tool.call.toolview": "@deepseek-ai/dsh-client-ui-tool",
2955
+ "conversation.input.right": "@deepseek-ai/dsh-client-ui-conversation",
2956
+ "conversation.composer.dock": "@deepseek-ai/dsh-client-ui-conversation"
2957
+ },
2958
+ services: {
2959
+ commandUi: "@deepseek-ai/dsh-client-ui-commands",
2960
+ connection: "@deepseek-ai/dsh-client-connection",
2961
+ locale: "@deepseek-ai/dsh-client-locale",
2962
+ modelDirectories: "@deepseek-ai/dsh-client-ui-model-selection",
2963
+ slots: "@deepseek-ai/dsh-client-ui-renderer"
2964
+ },
2965
+ markers: { composerStats: {
2966
+ attribute: "data-composer-stats",
2967
+ package: "@deepseek-ai/dsh-client-ui-chat"
2968
+ } },
2969
+ diagnostics: {
2970
+ package: "@deepseek-ai/dsh-client-ui-renderer",
2971
+ crashLog: "slot entry crashed in",
2972
+ errorAttribute: "data-slot-error",
2973
+ slotAttribute: "data-slot"
2974
+ },
2975
+ tokens: {
2976
+ package: "@deepseek-ai/dsh-client-ui-theme",
2977
+ names: [
2978
+ "--dsw-alias-bg-layer-1",
2979
+ "--dsw-alias-bg-skeleton",
2980
+ "--dsw-alias-border-l2",
2981
+ "--dsw-alias-border-l2-darkmode-thin",
2982
+ "--dsw-alias-interactive-bg-hover",
2983
+ "--dsw-alias-interactive-bg-hover-solid",
2984
+ "--dsw-alias-label-dimmed",
2985
+ "--dsw-alias-label-primary",
2986
+ "--dsw-alias-label-secondary",
2987
+ "--dsw-alias-label-tertiary",
2988
+ "--dsw-alias-state-error-primary",
2989
+ "--dsw-alias-state-success-primary",
2990
+ "--dsw-alias-state-warn-label",
2991
+ "--dsw-elevation-prominent",
2992
+ "--dsw-specific-menu"
2993
+ ]
2994
+ }
2995
+ };
2996
+
2997
+ //#endregion
2998
+ //#region src/client/host-icons.ts
2999
+ /** A decorative glyph the host lacks renders nothing rather than crashing. */
3000
+ const NoIcon = () => null;
3001
+ /**
3002
+ * Resolve one 16px glyph under either host naming.
3003
+ *
3004
+ * A glyph the host lacks renders nothing, and the lookup logs a
3005
+ * {@link HOST_CONTRACT_MISS} warning. The host E2E fails on that warning, so a
3006
+ * missing icon is reported instead of shipping as a blank space.
3007
+ * @param icons - the host primitives module (`import * as`), read by name.
3008
+ * @param name - the glyph name without prefix or size, e.g. `Sparkle`.
3009
+ * @returns the host's component, or one rendering nothing.
3010
+ */
3011
+ function hostIcon(icons, name) {
3012
+ const exports$1 = icons;
3013
+ const icon = exports$1[`Icon${name}Regular`] ?? exports$1[`Icon${name}16`];
3014
+ if (icon !== void 0) return icon;
3015
+ console.warn(`${HOST_CONTRACT_MISS}: ${HOST_CONTRACT.icons.package} exports neither Icon${name}Regular nor Icon${name}16`);
3016
+ return NoIcon;
3017
+ }
3018
+
3019
+ //#endregion
3020
+ //#region src/client/ImageGenerateToolview.tsx
3021
+ /** DSH 0.1.7 renamed IconSparkle16; resolving it by name keeps older hosts working too. */
3022
+ const SparkleIcon$1 = hostIcon(__deepseek_ai_dsh_client_ui_primitives, "Sparkle");
3023
+ /** Title prompt truncation budget (characters). */
3024
+ const PROMPT_MAX_LENGTH$1 = 60;
3025
+ /**
3026
+ * Build the ImageGallery loader over the `image` endpoint.
3027
+ * @param rpc - Connection RPC caller.
3028
+ * @returns loader resolving an attachment ref to a data URL.
3029
+ */
3030
+ function createImageLoader(rpc) {
3031
+ return (attachment) => callSubscriptionsAuth(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
3032
+ }
3033
+ /**
3034
+ * English-dictionary fallback for a missing locale seat (standalone renders);
3035
+ * the framework always supplies the namespace-bound one.
3036
+ * @param key - dictionary key.
3037
+ * @param params - `{name}` template params.
3038
+ * @returns the template with params substituted.
3039
+ */
3040
+ function fallbackTranslate$3(key, params) {
3041
+ let text = en[key];
3042
+ for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
3043
+ return text;
3044
+ }
3045
+ /** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
3046
+ function derivePrompt$1(argsRaw) {
3047
+ let parsed;
3048
+ try {
3049
+ parsed = JSON.parse(argsRaw);
3050
+ } catch {
3051
+ parsed = void 0;
3052
+ }
3053
+ let prompt;
3054
+ if (typeof parsed === "object" && parsed !== null) {
3055
+ const args = parsed;
3056
+ if (typeof args.prompt === "string" && args.prompt !== "") prompt = args.prompt;
3057
+ else for (const value of Object.values(args)) if (typeof value === "string" && value !== "") {
3058
+ prompt = value;
3059
+ break;
3060
+ }
3061
+ }
3062
+ const line = (prompt ?? argsRaw).split("\n", 1)[0] ?? "";
3063
+ return line.length > PROMPT_MAX_LENGTH$1 ? `${line.slice(0, PROMPT_MAX_LENGTH$1)}…` : line;
3064
+ }
3065
+ /** Flatten a settled result's text blocks (the degraded text-only route and the error line). */
3066
+ function resultText$1(block) {
3067
+ if (!("kind" in block)) return "";
3068
+ const parts = [];
3069
+ for (const part of block.content) if (part.type === "text") parts.push(part.text);
3070
+ if (parts.length === 0 && block.error !== void 0) parts.push(`${block.error.name}: ${block.error.code}`);
3071
+ return parts.join("\n");
3072
+ }
3073
+ /** Image attachments of a settled result; empty while running or on the text-only route. */
3074
+ function resultImages(block) {
3075
+ if (!("kind" in block)) return [];
3076
+ const images = [];
3077
+ for (const part of block.content) if (part.type === "image") images.push({ attachment: part.attachment });
3078
+ return images;
3079
+ }
3080
+ const styles$3 = {
3081
+ container: {
3082
+ display: "flex",
3083
+ flexDirection: "column",
3084
+ gap: 6,
3085
+ padding: "4px 0"
3086
+ },
3087
+ row: {
3088
+ display: "flex",
3089
+ alignItems: "center",
3090
+ gap: 6,
3091
+ minWidth: 0
3092
+ },
3093
+ icon: {
3094
+ display: "inline-flex",
3095
+ flexShrink: 0,
3096
+ color: "var(--dsw-alias-label-tertiary)"
3097
+ },
3098
+ title: {
3099
+ fontSize: 13,
3100
+ lineHeight: "20px",
3101
+ color: "var(--dsw-alias-label-primary)",
3102
+ overflow: "hidden",
3103
+ textOverflow: "ellipsis",
3104
+ whiteSpace: "nowrap"
3105
+ },
3106
+ subtle: {
3107
+ margin: 0,
3108
+ fontSize: 12,
3109
+ lineHeight: "18px",
3110
+ color: "var(--dsw-alias-label-tertiary)"
3111
+ },
3112
+ output: {
3113
+ margin: 0,
3114
+ fontSize: 12,
3115
+ lineHeight: "18px",
3116
+ color: "var(--dsw-alias-label-secondary)",
3117
+ whiteSpace: "pre-wrap",
3118
+ overflowWrap: "anywhere"
3119
+ },
3120
+ error: {
3121
+ margin: 0,
3122
+ fontSize: 12,
3123
+ lineHeight: "18px",
3124
+ color: "var(--dsw-alias-state-error-primary)"
3125
+ }
3126
+ };
3127
+ /**
3128
+ * The `image_generate` keyed toolview component.
3129
+ * @param props - owner share, inject face, and locale seat (spread flat).
3130
+ * @returns the call row plus, once settled, the gallery / text / error body.
3131
+ */
3132
+ function ImageGenerateToolview(props) {
3133
+ const { block, load } = props;
3134
+ const t = props.t ?? fallbackTranslate$3;
3135
+ if (block === void 0) return null;
3136
+ const settled = "kind" in block;
3137
+ const argsRaw = (settled ? block.call?.argsRaw : block.phase === "start" ? block.argsRaw : void 0) ?? "";
3138
+ let references = 0;
3139
+ try {
3140
+ const args = JSON.parse(argsRaw);
3141
+ if (Array.isArray(args?.referenceImages)) references = args.referenceImages.length;
3142
+ } catch {}
3143
+ const title = `image_generate${references > 0 ? ` (${references} ref)` : ""}: ${derivePrompt$1(argsRaw)}`;
3144
+ const images = resultImages(block);
3145
+ const text = settled ? resultText$1(block) : "";
3146
+ const labels = {
3147
+ image: t("image"),
3148
+ open: t("viewImage"),
3149
+ openNamed: (name) => t("viewImageNamed", { name }),
3150
+ loading: t("imageLoading"),
3151
+ loadFailed: t("imageLoadFailed"),
3152
+ lightbox: {
3153
+ dialog: t("imagePreview"),
3154
+ close: t("imageClose")
3155
+ }
3156
+ };
3157
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3158
+ style: styles$3.container,
3159
+ children: [
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3161
+ style: styles$3.row,
3162
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3163
+ style: styles$3.icon,
3164
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon$1, { size: 14 })
3165
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3166
+ style: styles$3.title,
3167
+ children: title
3168
+ })]
3169
+ }),
3170
+ !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3171
+ style: styles$3.subtle,
3172
+ children: t("generating")
3173
+ }),
3174
+ settled && block.isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3175
+ style: styles$3.error,
3176
+ children: text.split("\n", 1)[0]
3177
+ }),
3178
+ settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGallery, {
3179
+ images,
3180
+ load,
3181
+ labels
3182
+ }),
3183
+ settled && !block.isError && images.length === 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3184
+ style: styles$3.output,
3185
+ children: text
3186
+ })
3187
+ ]
3188
+ });
3189
+ }
3190
+
3191
+ //#endregion
3192
+ //#region src/client/VideoGenerateToolview.tsx
3193
+ /** DSH 0.1.7 renamed IconSparkle16; resolving it by name keeps older hosts working too. */
3194
+ const SparkleIcon = hostIcon(__deepseek_ai_dsh_client_ui_primitives, "Sparkle");
3195
+ /** Title prompt truncation budget (characters). */
3196
+ const PROMPT_MAX_LENGTH = 60;
3197
+ /**
3198
+ * Build the video loader over the `/subscriptions-auth` `video` endpoint.
3199
+ * @param rpc - Connection RPC caller.
3200
+ * @returns loader resolving a bare file name to the decoded bytes.
3201
+ */
3202
+ function createVideoLoader(rpc) {
3203
+ return (name) => callSubscriptionsAuth(rpc, "video", { name });
3204
+ }
3205
+ /**
3206
+ * English-dictionary fallback for a missing locale seat (standalone renders);
3207
+ * the framework always supplies the namespace-bound one.
3208
+ */
3209
+ function fallbackTranslate$2(key, params) {
3210
+ let text = en[key];
3211
+ for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
3212
+ return text;
3213
+ }
3214
+ /** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
3215
+ function derivePrompt(argsRaw) {
3216
+ let parsed;
3217
+ try {
3218
+ parsed = JSON.parse(argsRaw);
3219
+ } catch {
3220
+ parsed = void 0;
3221
+ }
3222
+ let prompt;
3223
+ if (typeof parsed === "object" && parsed !== null) {
3224
+ const args = parsed;
3225
+ if (typeof args.prompt === "string" && args.prompt !== "") prompt = args.prompt;
3226
+ else for (const value of Object.values(args)) if (typeof value === "string" && value !== "") {
3227
+ prompt = value;
3228
+ break;
3229
+ }
3230
+ }
3231
+ const line = (prompt ?? argsRaw).split("\n", 1)[0] ?? "";
3232
+ return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
3233
+ }
3234
+ /** Flatten a settled result's text blocks (the fallback body and the error line). */
3235
+ function resultText(block) {
3236
+ if (!("kind" in block)) return "";
3237
+ const parts = [];
3238
+ for (const part of block.content) if (part.type === "text") parts.push(part.text);
3239
+ if (parts.length === 0 && block.error !== void 0) parts.push(`${block.error.name}: ${block.error.code}`);
3240
+ return parts.join("\n");
3241
+ }
3242
+ /**
3243
+ * The generated video's bare file name: presentation meta first (top-level
3244
+ * dispatches), then the render text's "Saved video to …" line (nested
3245
+ * dispatches compute no meta).
3246
+ */
3247
+ function resolveFileName(block) {
3248
+ if (!("kind" in block)) return void 0;
3249
+ const meta = block.meta;
3250
+ if (typeof meta === "object" && meta !== null) {
3251
+ const fileName = meta.fileName;
3252
+ if (typeof fileName === "string" && fileName.length > 0) return fileName;
3253
+ }
3254
+ const match = /^Saved video to (.+\.mp4)/m.exec(resultText(block));
3255
+ if (match === null) return void 0;
3256
+ const path = match[1];
3257
+ return path.slice(path.lastIndexOf("/") + 1);
3258
+ }
3259
+ /** Decode a base64 payload into bytes (browser-side; no Buffer). */
3260
+ function base64Bytes(dataBase64) {
3261
+ const binary = atob(dataBase64);
3262
+ const bytes = new Uint8Array(binary.length);
3263
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
3264
+ return bytes;
3265
+ }
3266
+ const styles$2 = {
3267
+ container: {
3268
+ display: "flex",
3269
+ flexDirection: "column",
3270
+ gap: 6,
3271
+ padding: "4px 0"
3272
+ },
3273
+ row: {
3274
+ display: "flex",
3275
+ alignItems: "center",
3276
+ gap: 6,
3277
+ minWidth: 0
3278
+ },
3279
+ icon: {
3280
+ display: "inline-flex",
3281
+ flexShrink: 0,
3282
+ color: "var(--dsw-alias-label-tertiary)"
3283
+ },
3284
+ title: {
3285
+ fontSize: 13,
3286
+ lineHeight: "20px",
3287
+ color: "var(--dsw-alias-label-primary)",
3288
+ overflow: "hidden",
3289
+ textOverflow: "ellipsis",
3290
+ whiteSpace: "nowrap"
3291
+ },
3292
+ subtle: {
3293
+ margin: 0,
3294
+ fontSize: 12,
3295
+ lineHeight: "18px",
3296
+ color: "var(--dsw-alias-label-tertiary)"
3297
+ },
3298
+ output: {
3299
+ margin: 0,
3300
+ fontSize: 12,
3301
+ lineHeight: "18px",
3302
+ color: "var(--dsw-alias-label-secondary)",
3303
+ whiteSpace: "pre-wrap",
3304
+ overflowWrap: "anywhere"
3305
+ },
3306
+ error: {
3307
+ margin: 0,
3308
+ fontSize: 12,
3309
+ lineHeight: "18px",
3310
+ color: "var(--dsw-alias-state-error-primary)"
3311
+ },
3312
+ video: {
3313
+ display: "block",
3314
+ maxWidth: 480,
3315
+ width: "100%",
3316
+ borderRadius: 8,
3317
+ backgroundColor: "var(--dsw-alias-bg-skeleton)"
3318
+ }
3319
+ };
3320
+ /**
3321
+ * The `video_generate` keyed toolview component.
3322
+ * @param props - owner share, inject face, and locale seat (spread flat).
3323
+ * @returns the call row plus, once settled, the player / text / error body.
3324
+ */
3325
+ function VideoGenerateToolview(props) {
3326
+ const { block, loadVideo } = props;
3327
+ const t = props.t ?? fallbackTranslate$2;
3328
+ const settled = block !== void 0 && "kind" in block;
3329
+ const isError = settled && block.isError;
3330
+ const fileName = block !== void 0 && settled && !isError ? resolveFileName(block) : void 0;
3331
+ const [load, setLoad] = (0, react.useState)({ phase: "loading" });
3332
+ (0, react.useEffect)(() => {
3333
+ if (fileName === void 0 || loadVideo === void 0) return;
3334
+ let cancelled = false;
3335
+ let objectUrl;
3336
+ setLoad({ phase: "loading" });
3337
+ loadVideo(fileName).then((video) => {
3338
+ if (cancelled) return;
3339
+ objectUrl = URL.createObjectURL(new Blob([base64Bytes(video.dataBase64).slice()], { type: video.mediaType }));
3340
+ setLoad({
3341
+ phase: "ready",
3342
+ url: objectUrl
3343
+ });
3344
+ }, (error) => {
3345
+ if (cancelled) return;
3346
+ setLoad({
3347
+ phase: "failed",
3348
+ message: error instanceof Error ? error.message : String(error)
3349
+ });
3350
+ });
3351
+ return () => {
3352
+ cancelled = true;
3353
+ if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
3354
+ };
3355
+ }, [fileName, loadVideo]);
3356
+ if (block === void 0) return null;
3357
+ const title = `video_generate: ${derivePrompt((settled ? block.call?.argsRaw : block.phase === "start" ? block.argsRaw : void 0) ?? "")}`;
3358
+ const text = settled ? resultText(block) : "";
3359
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3360
+ style: styles$2.container,
3361
+ children: [
3362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3363
+ style: styles$2.row,
3364
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3365
+ style: styles$2.icon,
3366
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 })
3367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3368
+ style: styles$2.title,
3369
+ children: title
3370
+ })]
3371
+ }),
3372
+ !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3373
+ style: styles$2.subtle,
3374
+ children: t("generatingVideo")
3375
+ }),
3376
+ settled && isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3377
+ style: styles$2.error,
3378
+ children: text.split("\n", 1)[0]
3379
+ }),
3380
+ settled && !isError && fileName !== void 0 && load.phase === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3381
+ style: styles$2.subtle,
3382
+ children: t("videoLoading")
3383
+ }),
3384
+ settled && !isError && fileName !== void 0 && load.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3385
+ style: styles$2.error,
3386
+ children: t("videoLoadFailed", { message: load.message })
3387
+ }),
3388
+ settled && !isError && fileName !== void 0 && load.phase === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
3389
+ style: styles$2.video,
3390
+ src: load.url,
3391
+ controls: true,
3392
+ preload: "metadata"
3393
+ }),
3394
+ settled && !isError && fileName === void 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3395
+ style: styles$2.output,
3396
+ children: text
3397
+ })
3398
+ ]
3399
+ });
3400
+ }
3401
+
3402
+ //#endregion
3403
+ //#region src/client/SpeedSelect.tsx
3404
+ /**
3405
+ * The `loadSpeed` half of the inject face: the plugin's own speed state plus
3406
+ * the host's current model selection (the visibility gate). A model-RPC
3407
+ * failure throws rather than answering "hidden" — the caller keeps its last
3408
+ * known state, so a transient failure never locks the toggle away.
3409
+ *
3410
+ * `sessionId` is a plain string: slot and command contexts brand it through
3411
+ * different dsh-session copies, and only the service boundary needs one.
3412
+ *
3413
+ * `models` resolves lazily per call: the ui-model-selection service may
3414
+ * register after this plugin applies, and a shell without it (no model seat
3415
+ * at all) simply keeps the toggle hidden.
3416
+ */
3417
+ function createSpeedLoader(connection, models, sessionId) {
3418
+ return async () => {
3419
+ const state = await callSubscriptionsAuth(connection.rpc, "speed", { sessionId });
3420
+ const directories = models();
3421
+ if (directories === void 0) return {
3422
+ visible: false,
3423
+ tier: state.tier
3424
+ };
3425
+ const { current } = await directories.directoryFor(sessionId).load();
3426
+ return {
3427
+ visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
3428
+ tier: state.tier
3429
+ };
3430
+ };
3431
+ }
3432
+ /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
3433
+ function createSpeedSetter(connection, sessionId) {
3434
+ return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
3435
+ sessionId,
3436
+ tier
3437
+ }).then(() => true, () => false);
3438
+ }
3439
+ /** English-dictionary fallback for a missing inject `t` (standalone renders). */
3440
+ function fallbackTranslate$1(key) {
3441
+ return en[key];
3442
+ }
3443
+ const TIERS = ["standard", "fast"];
3444
+ /**
3445
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
3446
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
3447
+ * the current tier). Mount and every open reload the host state so a model
3448
+ * switch made since the last open self-corrects.
3449
+ */
3450
+ /** How often the control re-reads the host state (model switches arrive only by asking). */
3451
+ const POLL_INTERVAL_MS = 3e3;
3452
+ /**
3453
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
3454
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
3455
+ * the current tier). The host pushes nothing on a model switch, so the
3456
+ * control re-reads on a slow poll with a single-flight guard; a failed read
3457
+ * keeps the last known state, so a transient RPC failure can never lock the
3458
+ * toggle away (the earlier mount-only load had no recovery path).
3459
+ */
3460
+ function SpeedSelect({ loadSpeed, setSpeed, t }) {
3461
+ const translate = t ?? fallbackTranslate$1;
3462
+ const [state, setState] = (0, react.useState)(null);
3463
+ const [open, setOpen] = (0, react.useState)(false);
3464
+ const [busy, setBusy] = (0, react.useState)(false);
3465
+ const rootRef = (0, react.useRef)(null);
3466
+ const loadRef = (0, react.useRef)(loadSpeed);
3467
+ loadRef.current = loadSpeed;
3468
+ (0, react.useEffect)(() => {
3469
+ if (loadRef.current === void 0) return;
3470
+ let cancelled = false;
3471
+ let inflight = false;
3472
+ const reload = () => {
3473
+ const load = loadRef.current;
3474
+ if (load === void 0 || inflight) return;
3475
+ inflight = true;
3476
+ load().then((loaded) => {
3477
+ if (!cancelled) setState(loaded);
3478
+ }, () => {}).finally(() => {
3479
+ inflight = false;
3480
+ });
3481
+ };
3482
+ reload();
3483
+ const timer = setInterval(reload, POLL_INTERVAL_MS);
3484
+ return () => {
3485
+ cancelled = true;
3486
+ clearInterval(timer);
3487
+ };
3488
+ }, []);
3489
+ (0, react.useEffect)(() => {
3490
+ if (!open) return;
3491
+ const closeOutside = (event) => {
3492
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
3493
+ };
3494
+ document.addEventListener("mousedown", closeOutside);
3495
+ return () => {
3496
+ document.removeEventListener("mousedown", closeOutside);
3497
+ };
3498
+ }, [open]);
3499
+ if (loadSpeed === void 0 || setSpeed === void 0 || state === null || !state.visible) return null;
3500
+ const choose = (tier) => {
3501
+ if (busy) return;
3502
+ if (tier === state.tier) {
3503
+ setOpen(false);
3504
+ return;
3505
+ }
3506
+ setBusy(true);
3507
+ setSpeed(tier).then((ok) => {
3508
+ setBusy(false);
3509
+ if (ok) {
3510
+ setState({
3511
+ visible: true,
3512
+ tier
3513
+ });
3514
+ setOpen(false);
3515
+ }
3516
+ });
3517
+ };
3518
+ const show = () => {
3519
+ setOpen(true);
3520
+ const load = loadRef.current;
3521
+ if (load === void 0) return;
3522
+ load().then(setState, () => {});
3523
+ };
3524
+ const tierName = (tier) => translate(tier === "fast" ? "speedFast" : "speedStandard");
3525
+ const tierDescription = (tier) => translate(tier === "fast" ? "speedFastDescription" : "speedStandardDescription");
3526
+ const triggerLabel = `${translate("speed")} · ${tierName(state.tier)}`;
3527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3528
+ ref: rootRef,
3529
+ style: styles$1.root,
3530
+ onKeyDown: (event) => {
3531
+ if (event.key === "Escape" && open) {
3532
+ event.preventDefault();
3533
+ setOpen(false);
3534
+ }
3535
+ },
3536
+ children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3537
+ style: styles$1.menu,
3538
+ role: "menu",
3539
+ "aria-label": translate("speed"),
3540
+ children: TIERS.map((tier) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3541
+ type: "button",
3542
+ role: "menuitemradio",
3543
+ "aria-checked": tier === state.tier,
3544
+ style: styles$1.item,
3545
+ disabled: busy,
3546
+ onClick: () => {
3547
+ choose(tier);
3548
+ },
3549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3550
+ style: styles$1.itemCheck,
3551
+ children: tier === state.tier ? "✓" : ""
3552
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3553
+ style: styles$1.itemText,
3554
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3555
+ style: styles$1.itemName,
3556
+ children: tierName(tier)
3557
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3558
+ style: styles$1.itemDescription,
3559
+ children: tierDescription(tier)
3560
+ })]
3561
+ })]
3562
+ }, tier))
3563
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3564
+ type: "button",
3565
+ style: styles$1.trigger,
3566
+ "aria-haspopup": "menu",
3567
+ "aria-expanded": open,
3568
+ title: triggerLabel,
3569
+ disabled: busy,
3570
+ onClick: () => {
3571
+ if (open) setOpen(false);
3572
+ else show();
3573
+ },
3574
+ children: triggerLabel
3575
+ })]
3576
+ });
3577
+ }
3578
+ const styles$1 = {
3579
+ root: {
3580
+ position: "relative",
3581
+ display: "inline-flex"
3582
+ },
3583
+ trigger: {
3584
+ border: "1px solid var(--dsw-alias-border-l2)",
3585
+ borderRadius: 8,
3586
+ background: "transparent",
3587
+ color: "var(--dsw-alias-label-secondary)",
3588
+ font: "inherit",
3589
+ fontSize: 12,
3590
+ lineHeight: "18px",
3591
+ padding: "2px 8px",
3592
+ cursor: "pointer",
3593
+ whiteSpace: "nowrap"
3594
+ },
3595
+ menu: {
3596
+ position: "absolute",
3597
+ bottom: "100%",
3598
+ right: 0,
3599
+ marginBottom: 4,
3600
+ minWidth: 180,
3601
+ padding: 4,
3602
+ zIndex: 20,
3603
+ background: "var(--dsw-alias-bg-layer-1)",
3604
+ border: "1px solid var(--dsw-alias-border-l2)",
3605
+ borderRadius: 8,
3606
+ display: "flex",
3607
+ flexDirection: "column",
3608
+ gap: 2
3609
+ },
3610
+ item: {
3611
+ display: "flex",
3612
+ alignItems: "flex-start",
3613
+ gap: 6,
3614
+ width: "100%",
3615
+ border: "none",
3616
+ borderRadius: 6,
3617
+ background: "transparent",
3618
+ padding: "6px 8px",
3619
+ cursor: "pointer",
3620
+ font: "inherit",
3621
+ textAlign: "left"
3622
+ },
3623
+ itemCheck: {
3624
+ width: 14,
3625
+ flexShrink: 0,
3626
+ fontSize: 12,
3627
+ lineHeight: "18px",
3628
+ color: "var(--dsw-alias-label-primary)"
3629
+ },
3630
+ itemText: {
3631
+ display: "flex",
3632
+ flexDirection: "column"
3633
+ },
3634
+ itemName: {
3635
+ fontSize: 12,
3636
+ lineHeight: "18px",
3637
+ color: "var(--dsw-alias-label-primary)"
3638
+ },
3639
+ itemDescription: {
3640
+ fontSize: 11,
3641
+ lineHeight: "16px",
3642
+ color: "var(--dsw-alias-label-tertiary)"
3643
+ }
3644
+ };
3645
+
3646
+ //#endregion
3647
+ //#region src/client/SubscriptionUsageBadge.tsx
3648
+ /** DSH renamed the data icon in 0.1.7; retain older supported hosts too. */
3649
+ function usageBadgeIcon(icons) {
3650
+ return hostIcon(icons, "DataOutline");
3651
+ }
3652
+ const UsageBadgeIcon = usageBadgeIcon(__deepseek_ai_dsh_client_ui_primitives);
3653
+ /** How often the badge re-reads usage; the server also shares its own cache/negative-cache across UI surfaces. */
3654
+ const USAGE_POLL_INTERVAL_MS = 15 * 6e4;
3655
+ /** How often the badge re-reads the session's current model (model switches arrive only by asking). */
3656
+ const MODEL_POLL_INTERVAL_MS = 3e3;
3657
+ /** Distance between the trigger's top edge and the dialog's bottom (host stat dialogs use the same). */
3658
+ const PANEL_GAP = 8;
3659
+ /** Distance kept between the dialog and each viewport edge. */
3660
+ const PANEL_MARGIN = 12;
3661
+ /** The account the collapsed pill reads: the default one, else the first listed. */
3662
+ function pillAccountOf(d) {
3663
+ return d.accounts.find((a) => a.isDefault) ?? d.accounts[0];
3664
+ }
3665
+ const PROVIDER_NAMES = {
3666
+ codex: "Codex",
3667
+ claude: "Claude",
3668
+ grok: "Grok",
3669
+ copilot: "Copilot",
3670
+ antigravity: "Antigravity",
3671
+ "cursor-subscription": "Cursor",
3672
+ "opencode-go": "OpenCode Go",
3673
+ "kimi-coding": "Kimi Code"
3674
+ };
3675
+ /** Key of one account's cached windows: `provider:accountKey`. */
3676
+ function usageKeyOf(provider, account) {
3677
+ return `${provider}:${account.key}`;
3678
+ }
3679
+ /**
3680
+ * Group per-account usage into the badge's provider rows, in roster order
3681
+ * (the `status` provider order, default account first) so rows don't jump
3682
+ * around as polls settle at different times. An account without windows is
3683
+ * left out, and so is a provider whose accounts all lack windows.
3684
+ * @param roster - accounts from {@link loadBadgeRoster}.
3685
+ * @param windowsOf - each account's windows, keyed `provider:accountKey`.
3686
+ * @param plans - the plan each usage call reported, same keys; the roster's plan is the fallback.
3687
+ * @returns one display per provider with at least one account that has windows.
3688
+ */
3689
+ function groupUsageDisplays(roster, windowsOf, plans) {
3690
+ const byProvider = /* @__PURE__ */ new Map();
3691
+ for (const { provider, account } of roster) {
3692
+ const key = usageKeyOf(provider, account);
3693
+ const windows = windowsOf.get(key);
3694
+ if (windows === void 0) continue;
3695
+ const plan = plans.get(key) ?? account.plan;
3696
+ const row = {
3697
+ key: account.key,
3698
+ isDefault: account.isDefault,
3699
+ ...account.account === void 0 ? {} : { account: account.account },
3700
+ ...plan === void 0 ? {} : { plan },
3701
+ windows
3702
+ };
3703
+ const display = byProvider.get(provider);
3704
+ if (display === void 0) byProvider.set(provider, {
3705
+ provider,
3706
+ name: PROVIDER_NAMES[provider],
3707
+ accounts: [row]
3708
+ });
3709
+ else display.accounts.push(row);
3710
+ }
3711
+ return [...byProvider.values()];
3712
+ }
3713
+ /** Read each account source independently so a failing endpoint cannot hide the others. */
3714
+ async function loadBadgeRoster(rpc) {
3715
+ const [subscriptions, cursor, external] = await Promise.allSettled([
3716
+ callSubscriptionsAuth(rpc, "status", {}),
3717
+ callSubscriptionsAuth(rpc, "cursorStatus", {}),
3718
+ callSubscriptionsAuth(rpc, "externalStatus", {})
3719
+ ]);
3720
+ const roster = [];
3721
+ const refreshed = /* @__PURE__ */ new Set();
3722
+ if (subscriptions.status === "fulfilled") for (const provider of [
3723
+ "codex",
3724
+ "claude",
3725
+ "grok",
3726
+ "copilot",
3727
+ "antigravity"
3728
+ ]) {
3729
+ refreshed.add(provider);
3730
+ for (const account of accountsOf(subscriptions.value.providers[provider])) roster.push({
3731
+ provider,
3732
+ account
3733
+ });
3734
+ }
3735
+ if (cursor.status === "fulfilled") {
3736
+ refreshed.add("cursor-subscription");
3737
+ if (cursor.value.authenticated) roster.push({
3738
+ provider: "cursor-subscription",
3739
+ account: {
3740
+ key: "cursor",
3741
+ isDefault: true
3742
+ }
3743
+ });
3744
+ }
3745
+ if (external.status === "fulfilled") for (const [source, provider] of [["opencode-go", "opencode-go"], ["kimi-code", "kimi-coding"]]) {
3746
+ refreshed.add(provider);
3747
+ if (external.value[source]?.configured) roster.push({
3748
+ provider,
3749
+ account: {
3750
+ key: source,
3751
+ isDefault: true
3752
+ }
3753
+ });
3754
+ }
3755
+ return {
3756
+ roster,
3757
+ refreshed
3758
+ };
3759
+ }
3760
+ async function usageOf(rpc, { provider, account }) {
3761
+ if (provider === "cursor-subscription") return callSubscriptionsAuth(rpc, "cursorUsage", {});
3762
+ if (provider === "opencode-go" || provider === "kimi-coding") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
3763
+ return callSubscriptionsAuth(rpc, "usage", {
3764
+ provider,
3765
+ account: account.key
3766
+ });
3767
+ }
3768
+ /**
3769
+ * The `currentModel` half of the inject face: the session's effective
3770
+ * model selection through ui-model-selection's `modelDirectories` service,
3771
+ * resolved lazily per call (the service may register after this plugin, and
3772
+ * a shell without it reports "unknown"; the badge keeps the most recent
3773
+ * subscription selected in this view, or stays hidden).
3774
+ */
3775
+ function createCurrentModelReader(models, sessionId) {
3776
+ return async () => {
3777
+ const directories = models();
3778
+ if (directories === void 0) return void 0;
3779
+ const { current } = await directories.directoryFor(sessionId).load();
3780
+ return current ?? void 0;
3781
+ };
3782
+ }
3783
+ /**
3784
+ * Compact time-remaining label derived from the window's `resetsAt` timestamp:
3785
+ * "6d18h" (days+hours), "1h58m" (hours+minutes), or "42m" (minutes only).
3786
+ * Falls back to the scope/kind abbreviation when no reset time is known.
3787
+ */
3788
+ function windowLabel(w) {
3789
+ if (w.resetsAt === void 0) {
3790
+ if (w.scope !== void 0 && w.scope !== "") return w.scope;
3791
+ switch (w.kind) {
3792
+ case "session": return "5h";
3793
+ case "weekly": return "Wk";
3794
+ default: return "W";
3795
+ }
3796
+ }
3797
+ const ms = Math.max(0, w.resetsAt - Date.now());
3798
+ const minutes = Math.floor(ms / 6e4);
3799
+ const hours = Math.floor(minutes / 60);
3800
+ const days = Math.floor(hours / 24);
3801
+ if (days > 0) return `${days}d${hours % 24}h`;
3802
+ if (hours > 0) return `${hours}h${minutes % 60}m`;
3803
+ return `${Math.max(1, minutes)}m`;
3804
+ }
3805
+ /** Clamp and round a window's used share for display. */
3806
+ function usedPercent(w) {
3807
+ return Math.round(Math.min(100, Math.max(0, w.usedPercent)));
3808
+ }
3809
+ /** Keep model quotas separate: matching percentages do not imply a shared pool. */
3810
+ function prioritizeWindows(windows, model) {
3811
+ return model === void 0 ? [...windows] : [...windows.filter((w) => w.scope === model), ...windows.filter((w) => w.scope !== model)];
3812
+ }
3813
+ /** Small previews keep a live model catalog from taking over the dialog. */
3814
+ const WINDOW_PREVIEW_LIMIT = 4;
3815
+ function previewWindows(windows, model, provider) {
3816
+ const ordered = prioritizeWindows(windows, model);
3817
+ const limit = provider === "antigravity" ? Math.min(2, model === void 0 ? 0 : windows.filter((w) => w.scope === model).length) : WINDOW_PREVIEW_LIMIT;
3818
+ return {
3819
+ shown: ordered.slice(0, limit),
3820
+ hidden: ordered.slice(limit)
3821
+ };
3822
+ }
3823
+ /** Bounded readout; Antigravity quotas belong to individual models, not the account. */
3824
+ function compactSegment(d, model, t = fallbackTranslate) {
3825
+ const windows = pillAccountOf(d).windows;
3826
+ if (d.provider === "antigravity") {
3827
+ const matching = model === void 0 ? [] : windows.filter((w) => w.scope === model);
3828
+ if (matching.length === 0) return `${d.name} ${t(model === void 0 ? "usageBadgeModelCount" : "usageBadgeModelUnavailable", { count: new Set(windows.map((w) => w.scope).filter(Boolean)).size })}`;
3829
+ const parts$1 = matching.slice(0, 2).map((w) => `${w.kind === "weekly" ? t("usageWeekly") : t("usageWindow")} ${usedPercent(w)}%`);
3830
+ return `${d.name} ${parts$1.join(" · ")}`;
3831
+ }
3832
+ const parts = windows.slice(0, 2).map((w) => `${windowLabel(w)} ${usedPercent(w)}%`);
3833
+ if (windows.length > 2) parts.push(`+${windows.length - 2}`);
3834
+ return `${d.name} ${parts.join(" · ")}`;
3835
+ }
3836
+ /** Remember only the most recent subscription in this view, including its model scope. */
3837
+ function retainSubscriptionSelection(previous, current) {
3838
+ return current !== void 0 && Object.hasOwn(PROVIDER_NAMES, current.provider) ? current : previous;
3839
+ }
3840
+ /** At most one provider; no known selection or no usage means no badge. */
3841
+ function collapsedDisplays(displays, current) {
3842
+ const match = displays.find((d) => d.provider === current);
3843
+ return match === void 0 ? [] : [match];
3844
+ }
3845
+ /** Order for the expanded dialog: the current provider first, the rest in poll order. */
3846
+ function expandedDisplays(displays, current) {
3847
+ const match = displays.find((d) => d.provider === current);
3848
+ return match === void 0 ? displays : [match, ...displays.filter((d) => d !== match)];
3849
+ }
3850
+ /**
3851
+ * A provider's logged-in accounts, the effective default first. When no
3852
+ * account is flagged default the first listed stands in, matching what
3853
+ * direct routes fall back to.
3854
+ */
3855
+ function accountsOf(status) {
3856
+ if (status === void 0 || status.accounts.length === 0) return [];
3857
+ const fallback = status.accounts.find((a) => a.isDefault) ?? status.accounts[0];
3858
+ return status.accounts.map((a) => a === fallback ? {
3859
+ ...a,
3860
+ isDefault: true
3861
+ } : a).sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
3862
+ }
3863
+ /** English-dictionary fallback for a missing inject `t` (standalone renders). */
3864
+ function fallbackTranslate(key, params) {
3865
+ return en[key].replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? ""));
3866
+ }
3867
+ /** Localized label of one usage window (kind, plus the model scope when named). */
3868
+ function usageWindowLabel(t, window$1) {
3869
+ const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
3870
+ return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
3871
+ }
3872
+ /**
3873
+ * The composer subscription-usage badge: a pill reading e.g.
3874
+ * `Codex 6d1h 25%` for the current model's provider, opening a dialog with
3875
+ * every provider's accounts and their windows. Returns null when no data is
3876
+ * available.
3877
+ */
3878
+ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3879
+ const translate = t ?? fallbackTranslate;
3880
+ const [displays, setDisplays] = (0, react.useState)([]);
3881
+ const [selection, setSelection] = (0, react.useState)(void 0);
3882
+ const [lastSubscription, setLastSubscription] = (0, react.useState)(void 0);
3883
+ const displayMode = useUsageBadgeMode();
3884
+ const current = selection?.provider;
3885
+ const badgeSelection = retainSubscriptionSelection(lastSubscription, selection);
3886
+ const [open, setOpen] = (0, react.useState)(false);
3887
+ const [hover, setHover] = (0, react.useState)(false);
3888
+ const inflightRef = (0, react.useRef)(false);
3889
+ const mountedRef = (0, react.useRef)(true);
3890
+ const rootRef = (0, react.useRef)(null);
3891
+ const panelRef = (0, react.useRef)(null);
3892
+ const seatRef = (0, react.useRef)(null);
3893
+ const currentRef = (0, react.useRef)(currentModel);
3894
+ currentRef.current = currentModel;
3895
+ const lastKnownRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3896
+ const refresh = (0, react.useCallback)(async () => {
3897
+ if (rpc === void 0 || inflightRef.current) return;
3898
+ inflightRef.current = true;
3899
+ try {
3900
+ const { roster, refreshed } = await loadBadgeRoster(rpc);
3901
+ if (!mountedRef.current) return;
3902
+ const lastKnown = lastKnownRef.current;
3903
+ const live = new Set(roster.map(({ provider, account }) => usageKeyOf(provider, account)));
3904
+ for (const key of lastKnown.keys()) if (refreshed.has(key.split(":", 1)[0]) && !live.has(key)) lastKnown.delete(key);
3905
+ const results = await Promise.allSettled(roster.map(async ({ provider, account }) => {
3906
+ return {
3907
+ provider,
3908
+ account,
3909
+ usage: await usageOf(rpc, {
3910
+ provider,
3911
+ account
3912
+ })
3913
+ };
3914
+ }));
3915
+ if (!mountedRef.current) return;
3916
+ const plans = /* @__PURE__ */ new Map();
3917
+ for (const r of results) {
3918
+ if (r.status !== "fulfilled") continue;
3919
+ const { provider, account, usage } = r.value;
3920
+ const key = usageKeyOf(provider, account);
3921
+ if (usage.plan !== void 0) plans.set(key, usage.plan);
3922
+ if (!usage.supported || !usage.windows || usage.windows.length === 0) {
3923
+ lastKnown.delete(key);
3924
+ continue;
3925
+ }
3926
+ lastKnown.set(key, usage.windows);
3927
+ }
3928
+ setDisplays((previous) => [...groupUsageDisplays(roster, lastKnown, plans), ...previous.filter((display) => !refreshed.has(display.provider))]);
3929
+ } catch {} finally {
3930
+ inflightRef.current = false;
3931
+ }
3932
+ }, [rpc]);
3933
+ (0, react.useEffect)(() => {
3934
+ mountedRef.current = true;
3935
+ return () => {
3936
+ mountedRef.current = false;
3937
+ };
3938
+ }, []);
3939
+ (0, react.useEffect)(() => {
3940
+ if (displayMode === "hidden") return;
3941
+ refresh();
3942
+ const timer = setInterval(() => {
3943
+ refresh();
3944
+ }, USAGE_POLL_INTERVAL_MS);
3945
+ window.addEventListener(USAGE_BADGE_REFRESH_EVENT, refresh);
3946
+ return () => {
3947
+ clearInterval(timer);
3948
+ window.removeEventListener(USAGE_BADGE_REFRESH_EVENT, refresh);
3949
+ };
3950
+ }, [refresh, displayMode]);
3951
+ (0, react.useEffect)(() => {
3952
+ if (currentRef.current === void 0) return;
3953
+ let cancelled = false;
3954
+ let inflight = false;
3955
+ const reload = () => {
3956
+ const read = currentRef.current;
3957
+ if (read === void 0 || inflight) return;
3958
+ inflight = true;
3959
+ read().then((model) => {
3960
+ if (cancelled) return;
3961
+ setLastSubscription((previous) => retainSubscriptionSelection(previous, model));
3962
+ setSelection(model);
3963
+ }, () => {}).finally(() => {
3964
+ inflight = false;
3965
+ });
3966
+ };
3967
+ reload();
3968
+ const timer = setInterval(reload, MODEL_POLL_INTERVAL_MS);
3969
+ return () => {
3970
+ cancelled = true;
3971
+ clearInterval(timer);
3972
+ };
3973
+ }, []);
3974
+ const pos = (0, __deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
3975
+ open,
3976
+ anchorRef: rootRef,
3977
+ panelRef,
3978
+ side: "top",
3979
+ gap: PANEL_GAP,
3980
+ margin: PANEL_MARGIN
3981
+ });
3982
+ (0, __deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen, panelRef);
3983
+ (0, react.useEffect)(() => {
3984
+ if (!open) return;
3985
+ const onKeyDown = (event) => {
3986
+ if (event.key === "Escape") setOpen(false);
3987
+ };
3988
+ document.addEventListener("keydown", onKeyDown);
3989
+ return () => {
3990
+ document.removeEventListener("keydown", onKeyDown);
3991
+ };
3992
+ }, [open]);
3993
+ const [statsRow, setStatsRow] = (0, react.useState)(null);
3994
+ (0, react.useEffect)(() => {
3995
+ const seat$1 = seatRef.current;
3996
+ if (seat$1 === null) return;
3997
+ const scope = statsScopeOf(seat$1);
3998
+ if (scope === null) return;
3999
+ const find = () => scope.querySelector("[data-composer-stats]");
4000
+ setStatsRow(find());
4001
+ const observer = new MutationObserver(() => {
4002
+ setStatsRow(find());
4003
+ });
4004
+ observer.observe(scope, {
4005
+ childList: true,
4006
+ subtree: true
4007
+ });
4008
+ return () => {
4009
+ observer.disconnect();
4010
+ };
4011
+ }, []);
4012
+ (0, react.useEffect)(() => {
4013
+ if (displayMode === "hidden") setOpen(false);
4014
+ }, [displayMode]);
4015
+ const seat = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4016
+ ref: seatRef,
4017
+ style: styles.seat,
4018
+ "aria-hidden": true
4019
+ });
4020
+ const collapsed = collapsedDisplays(displays, badgeSelection?.provider);
4021
+ if (displayMode === "hidden" || collapsed.length === 0) return seat;
4022
+ const label = compactSegment(collapsed[0], badgeSelection?.model, translate);
4023
+ const expanded = expandedDisplays(displays, badgeSelection?.provider);
4024
+ const title = translate("usageBadgeTitle");
4025
+ const toggle = () => {
4026
+ const next = !open;
4027
+ setOpen(next);
4028
+ if (next) refresh();
4029
+ };
4030
+ const pill = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4031
+ ref: rootRef,
4032
+ style: styles.anchor,
4033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4034
+ type: "button",
4035
+ style: {
4036
+ ...styles.pill,
4037
+ ...hover || open ? styles.pillActive : {}
4038
+ },
4039
+ "aria-haspopup": "dialog",
4040
+ "aria-expanded": open,
4041
+ "aria-label": `${title} · ${label}`,
4042
+ title,
4043
+ onMouseEnter: () => {
4044
+ setHover(true);
4045
+ },
4046
+ onMouseLeave: () => {
4047
+ setHover(false);
4048
+ },
4049
+ onClick: toggle,
4050
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBadgeIcon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4051
+ style: styles.label,
4052
+ children: label
4053
+ })]
4054
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4055
+ ref: panelRef,
4056
+ role: "dialog",
4057
+ "aria-label": title,
4058
+ style: {
4059
+ ...styles.panel,
4060
+ ...pos ?? MEASURE_STYLE
4061
+ },
4062
+ children: [
4063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4064
+ style: styles.title,
4065
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4066
+ style: styles.titleLabel,
4067
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBadgeIcon, {}), title]
4068
+ })
4069
+ }),
4070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4071
+ style: styles.titleRule,
4072
+ "aria-hidden": true
4073
+ }),
4074
+ expanded.map((d, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4075
+ style: index === 0 ? void 0 : styles.section,
4076
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4077
+ style: styles.providerRow,
4078
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4079
+ style: styles.providerName,
4080
+ children: [d.name, d.provider === current && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4081
+ style: styles.currentTag,
4082
+ children: translate("usageBadgeCurrent")
4083
+ })]
4084
+ }), d.accounts.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountMeta, {
4085
+ account: d.accounts[0],
4086
+ translate
4087
+ })]
4088
+ }), d.accounts.map((account, accountIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4089
+ style: accountIndex === 0 ? void 0 : styles.accountBlock,
4090
+ children: [d.accounts.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4091
+ style: styles.accountRow,
4092
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountMeta, {
4093
+ account,
4094
+ translate
4095
+ })
4096
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountWindows, {
4097
+ windows: account.windows,
4098
+ model: d.provider === badgeSelection?.provider ? badgeSelection.model : void 0,
4099
+ provider: d.provider,
4100
+ translate
4101
+ }, `${d.provider}:${badgeSelection?.model ?? ""}`)]
4102
+ }, account.key))]
4103
+ }, d.provider))
4104
+ ]
4105
+ }), document.body)]
4106
+ });
4107
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [seat, statsRow !== null && statsRow.isConnected ? (0, react_dom.createPortal)(pill, statsRow) : pill] });
4108
+ }
4109
+ /**
4110
+ * Nearest ancestor of the dock seat that can contain the host's stats row:
4111
+ * the composer bar. Bounded so a badge in an unfamiliar layout never adopts
4112
+ * some other composer's pills.
4113
+ */
4114
+ function statsScopeOf(seat) {
4115
+ let node = seat.parentElement;
4116
+ for (let depth = 0; node !== null && depth < 4; depth++) {
4117
+ if (node.querySelector("[data-composer-stats]") !== null) return node;
4118
+ node = node.parentElement;
4119
+ }
4120
+ return seat.parentElement;
4121
+ }
4122
+ /**
4123
+ * One account's handle and plan: `★ ys@example.com · 计划:pro`. The star
4124
+ * marks the default account (the one direct routes serve and the collapsed
4125
+ * pill reads), the same glyph Settings → 订阅 uses.
4126
+ */
4127
+ function AccountMeta({ account, translate }) {
4128
+ const parts = [account.account, account.plan === void 0 ? void 0 : translate("usagePlan", { plan: account.plan })].filter((part) => part !== void 0 && part !== "");
4129
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4130
+ style: styles.providerMeta,
4131
+ title: account.account,
4132
+ children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4133
+ style: styles.defaultStar,
4134
+ "aria-label": "default",
4135
+ children: "★ "
4136
+ }), parts.join(" · ")]
4137
+ });
4138
+ }
4139
+ /** Preview each account independently; all remaining quotas stay accessible. */
4140
+ function AccountWindows({ windows, model, provider, translate }) {
4141
+ const { shown, hidden } = previewWindows(windows, model, provider);
4142
+ const rows = (items) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
4143
+ style: styles.details,
4144
+ children: items.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WindowRow, {
4145
+ label: `${usageWindowLabel(translate, w)}${model !== void 0 && w.scope === model ? ` · ${translate("usageBadgeCurrent")}` : ""}`,
4146
+ window: w
4147
+ }, i))
4148
+ });
4149
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [rows(shown), hidden.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
4150
+ style: styles.moreWindows,
4151
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
4152
+ style: styles.moreSummary,
4153
+ children: translate("usageBadgeMoreWindows", { count: hidden.length })
4154
+ }), rows(hidden)]
4155
+ })] });
4156
+ }
4157
+ /** One `dt`/`dd` pair: window name → `25% · 6d1h`, with the bar underneath. */
4158
+ function WindowRow({ label, window: w }) {
4159
+ const percent = usedPercent(w);
4160
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
4162
+ style: styles.dt,
4163
+ children: label
4164
+ }),
4165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
4166
+ style: styles.dd,
4167
+ children: [
4168
+ percent,
4169
+ "%",
4170
+ w.resetsAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4171
+ style: styles.reset,
4172
+ children: [" · ", windowLabel(w)]
4173
+ })
4174
+ ]
4175
+ }),
4176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4177
+ style: styles.bar,
4178
+ "aria-hidden": true,
4179
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
4180
+ ...styles.barFill,
4181
+ width: `${percent}%`,
4182
+ background: usageBarColor(percent)
4183
+ } })
4184
+ })
4185
+ ] });
4186
+ }
4187
+ /**
4188
+ * Unplaced portal panel: hidden but laid out so the clamp measures real
4189
+ * dimensions (the `useAnchoredPosition` measure pass).
4190
+ */
4191
+ const MEASURE_STYLE = {
4192
+ visibility: "hidden",
4193
+ left: 0,
4194
+ top: 0
4195
+ };
4196
+ const styles = {
4197
+ seat: { display: "none" },
4198
+ anchor: {
4199
+ minWidth: 0,
4200
+ maxWidth: "100%",
4201
+ display: "inline-flex"
4202
+ },
4203
+ pill: {
4204
+ boxSizing: "border-box",
4205
+ maxWidth: "100%",
4206
+ color: "var(--dsw-alias-label-tertiary)",
4207
+ font: "inherit",
4208
+ fontSize: "var(--dsh-content-font-size-secondary, 13px)",
4209
+ fontVariantNumeric: "tabular-nums",
4210
+ lineHeight: "20px",
4211
+ whiteSpace: "nowrap",
4212
+ background: "transparent",
4213
+ border: "none",
4214
+ borderRadius: 24,
4215
+ alignItems: "center",
4216
+ gap: 6,
4217
+ padding: "1px 8px",
4218
+ display: "inline-flex",
4219
+ cursor: "pointer"
4220
+ },
4221
+ pillActive: {
4222
+ background: "var(--dsw-alias-interactive-bg-hover)",
4223
+ color: "var(--dsw-alias-label-secondary)"
4224
+ },
4225
+ label: {
4226
+ textOverflow: "ellipsis",
4227
+ minWidth: 0,
4228
+ overflow: "hidden"
4229
+ },
4230
+ panel: {
4231
+ position: "fixed",
4232
+ zIndex: 1100,
4233
+ boxSizing: "border-box",
4234
+ background: "var(--dsw-specific-menu)",
4235
+ width: "max-content",
4236
+ minWidth: "min(300px, 100vw - 24px)",
4237
+ maxWidth: "min(440px, 100vw - 24px)",
4238
+ maxHeight: "min(560px, 100dvh - 24px)",
4239
+ overflowY: "auto",
4240
+ overscrollBehavior: "contain",
4241
+ boxShadow: "var(--dsw-elevation-prominent)",
4242
+ color: "var(--dsw-alias-label-secondary)",
4243
+ cursor: "default",
4244
+ border: 0,
4245
+ borderRadius: 12,
4246
+ padding: 16,
4247
+ fontSize: 12,
4248
+ lineHeight: "18px"
4249
+ },
4250
+ title: {
4251
+ color: "var(--dsw-alias-label-primary)",
4252
+ display: "flex",
4253
+ justifyContent: "space-between",
4254
+ gap: 16,
4255
+ marginBottom: 8,
4256
+ fontWeight: 500
4257
+ },
4258
+ titleLabel: {
4259
+ alignItems: "center",
4260
+ gap: 6,
4261
+ minWidth: 0,
4262
+ display: "inline-flex"
4263
+ },
4264
+ titleRule: {
4265
+ borderTop: "0.5px solid var(--dsw-alias-border-l2)",
4266
+ marginBottom: 10
4267
+ },
4268
+ section: {
4269
+ marginTop: 12,
4270
+ paddingTop: 10,
4271
+ borderTop: "0.5px solid var(--dsw-alias-border-l2)"
4272
+ },
4273
+ providerRow: {
4274
+ display: "flex",
4275
+ justifyContent: "space-between",
4276
+ alignItems: "baseline",
4277
+ gap: 16,
4278
+ marginBottom: 6
4279
+ },
4280
+ providerName: {
4281
+ color: "var(--dsw-alias-label-primary)",
4282
+ fontWeight: 500,
4283
+ display: "inline-flex",
4284
+ alignItems: "center",
4285
+ gap: 6
4286
+ },
4287
+ currentTag: {
4288
+ fontSize: 10,
4289
+ lineHeight: "14px",
4290
+ fontWeight: 400,
4291
+ padding: "0 5px",
4292
+ borderRadius: 7,
4293
+ color: "var(--dsw-alias-label-secondary)",
4294
+ background: "var(--dsw-alias-interactive-bg-hover)"
4295
+ },
4296
+ providerMeta: {
4297
+ color: "var(--dsw-alias-label-tertiary)",
4298
+ minWidth: 0,
4299
+ overflow: "hidden",
4300
+ textOverflow: "ellipsis",
4301
+ whiteSpace: "nowrap"
4302
+ },
4303
+ defaultStar: { color: "var(--dsw-alias-state-warn-label)" },
4304
+ accountBlock: { marginTop: 8 },
4305
+ accountRow: {
4306
+ display: "flex",
4307
+ marginBottom: 4
4308
+ },
4309
+ details: {
4310
+ color: "var(--dsw-alias-label-tertiary)",
4311
+ display: "grid",
4312
+ gridTemplateColumns: "minmax(0, 1fr) max-content",
4313
+ gap: "4px 16px",
4314
+ margin: 0
4315
+ },
4316
+ moreWindows: { marginTop: 8 },
4317
+ moreSummary: {
4318
+ cursor: "pointer",
4319
+ color: "var(--dsw-alias-label-secondary)",
4320
+ marginBottom: 8
4321
+ },
4322
+ dt: {
4323
+ minWidth: 0,
4324
+ margin: 0,
4325
+ overflowWrap: "anywhere"
4326
+ },
4327
+ dd: {
4328
+ minWidth: 0,
4329
+ margin: 0,
4330
+ color: "var(--dsw-alias-label-secondary)",
4331
+ fontVariantNumeric: "tabular-nums",
4332
+ textAlign: "right"
4333
+ },
4334
+ reset: { color: "var(--dsw-alias-label-tertiary)" },
4335
+ bar: {
4336
+ gridColumn: "1 / -1",
4337
+ height: 4,
4338
+ borderRadius: 2,
4339
+ overflow: "hidden",
4340
+ background: "var(--dsw-alias-border-l2)",
4341
+ marginBottom: 2
4342
+ },
4343
+ barFill: {
4344
+ height: "100%",
4345
+ borderRadius: 2
4346
+ }
4347
+ };
4348
+
4349
+ //#endregion
4350
+ //#region src/client/fast-command.ts
4351
+ /** Build the locale resolver used by the /fast command contribution. */
4352
+ function fastCommandDescription(t) {
4353
+ return () => t();
4354
+ }
4355
+
4356
+ //#endregion
4357
+ //#region src/client/index.ts
4358
+ /** Dictionary namespace owned by this plugin. */
4359
+ const NS = "settings.subscriptions";
4360
+ /**
4361
+ * Required services (cordis fiber inject): `slots` carries the registration
4362
+ * seat, `connection` the `/subscriptions-auth` RPC caller, and `locale` the copy
4363
+ * dictionaries.
4364
+ */
4365
+ const inject = [
4366
+ "slots",
4367
+ "connection",
4368
+ "locale"
4369
+ ];
4370
+ /**
4371
+ * Register the Subscriptions section once the `settings.section` declaration
4372
+ * is on the ledger (the shell's apply order relative to this one is NOT
4373
+ * constrained; registration depends on the slot through `slots.inject()`).
4374
+ * @param ctx - client root context.
4375
+ */
4376
+ function apply(ctx) {
4377
+ ctx.effect(() => ctx.locale.register(NS, {
4378
+ zh,
4379
+ en
4380
+ }), "dsh-subscriptions: copy dictionaries");
4381
+ ctx.effect(() => {
4382
+ const style = document.createElement("style");
4383
+ style.setAttribute("data-plugin", "dsh-subscriptions");
4384
+ style.textContent = "div[role=\"dialog\"][aria-modal=\"true\"]:has(> nav) { padding-top: 14px; }";
4385
+ document.head.appendChild(style);
4386
+ return () => style.remove();
4387
+ }, "dsh-subscriptions: settings panel breathing room");
4388
+ const connection = ctx.get("connection");
4389
+ const t = ctx.locale.bind(NS);
4390
+ const injected = () => ({
4391
+ rpc: connection.rpc,
4392
+ t
4393
+ });
4394
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
4395
+ name: "settings.section",
4396
+ id: "subscriptions",
4397
+ order: 90,
4398
+ label: () => t("nav"),
4399
+ inject: injected
4400
+ }, SubscriptionsSection));
4401
+ const toolviewInjected = () => ({ load: createImageLoader(connection.rpc) });
4402
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
4403
+ name: "tool.call.toolview",
4404
+ key: "image_generate",
4405
+ locale: NS,
4406
+ inject: toolviewInjected
4407
+ }, ImageGenerateToolview));
4408
+ const videoToolviewInjected = () => ({ loadVideo: createVideoLoader(connection.rpc) });
4409
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
4410
+ name: "tool.call.toolview",
4411
+ key: "video_generate",
4412
+ locale: NS,
4413
+ inject: videoToolviewInjected
4414
+ }, VideoGenerateToolview));
4415
+ const models = () => ctx.get("modelDirectories");
4416
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
4417
+ name: "conversation.input.right",
4418
+ id: "codex-speed",
4419
+ order: 0,
4420
+ locale: NS,
4421
+ inject: (sessionId) => ({
4422
+ loadSpeed: createSpeedLoader(connection, models, sessionId),
4423
+ setSpeed: createSpeedSetter(connection, sessionId)
4424
+ })
4425
+ }, SpeedSelect));
4426
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
4427
+ name: "conversation.composer.dock",
4428
+ id: "subscription-usage",
4429
+ order: 10,
4430
+ locale: NS,
4431
+ inject: (sessionId) => ({
4432
+ rpc: connection.rpc,
4433
+ currentModel: createCurrentModelReader(models, sessionId)
4434
+ })
4435
+ }, SubscriptionUsageBadge));
4436
+ ctx.inject(["commandUi"], (scope) => {
4437
+ const command = scope.get("commandUi");
4438
+ scope.effect(() => command.register({
4439
+ name: "fast",
4440
+ description: fastCommandDescription(() => t("commandFast")),
4441
+ available: () => true,
4442
+ ui: {
4443
+ kind: "popupSelect",
4444
+ options: async (session) => {
4445
+ const state = await createSpeedLoader(connection, models, session.sessionId)();
4446
+ if (!state.visible) throw new Error(t("commandFastUnavailable"));
4447
+ return [{
4448
+ id: "standard",
4449
+ label: t("speedStandard"),
4450
+ detail: t("speedStandardDescription")
4451
+ }, {
4452
+ id: "fast",
4453
+ label: t("speedFast"),
4454
+ detail: t("speedFastDescription")
4455
+ }].map((option) => ({
4456
+ ...option,
4457
+ active: option.id === state.tier
4458
+ }));
4459
+ },
4460
+ onSelect: async (option, session) => {
4461
+ await createSpeedSetter(connection, session.sessionId)(option.id);
4462
+ }
4463
+ }
4464
+ }), "dsh-subscriptions: /fast contribution");
4465
+ });
4466
+ }
4467
+
4468
+ //#endregion
4469
+ exports.apply = apply;
4470
+ exports.inject = inject;
4471
+ return module.exports; } });
4472
+ //# sourceMappingURL=client.js.map