dsh-plugin-subscription-usage 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +38 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +9 -0
  5. package/lib/auth/claude-code-creds.d.ts +23 -0
  6. package/lib/auth/claude-code-creds.js +172 -0
  7. package/lib/auth/device-flow.d.ts +55 -0
  8. package/lib/auth/device-flow.js +177 -0
  9. package/lib/auth/jwt.d.ts +10 -0
  10. package/lib/auth/jwt.js +25 -0
  11. package/lib/auth/oauth-flow.d.ts +91 -0
  12. package/lib/auth/oauth-flow.js +227 -0
  13. package/lib/auth/pkce.d.ts +31 -0
  14. package/lib/auth/pkce.js +35 -0
  15. package/lib/auth/rpc.d.ts +233 -0
  16. package/lib/auth/rpc.js +501 -0
  17. package/lib/auth/store.d.ts +198 -0
  18. package/lib/auth/store.js +473 -0
  19. package/lib/client/Footer.d.ts +3 -0
  20. package/lib/client/Footer.js +73 -0
  21. package/lib/client/Gauge.d.ts +28 -0
  22. package/lib/client/Gauge.js +139 -0
  23. package/lib/client/ImageGallery.d.ts +54 -0
  24. package/lib/client/ImageGallery.js +112 -0
  25. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  26. package/lib/client/ImageGenerateToolview.js +130 -0
  27. package/lib/client/Notice.d.ts +15 -0
  28. package/lib/client/Notice.js +93 -0
  29. package/lib/client/ProviderCard.d.ts +35 -0
  30. package/lib/client/ProviderCard.js +240 -0
  31. package/lib/client/SpeedSelect.d.ts +72 -0
  32. package/lib/client/SpeedSelect.js +214 -0
  33. package/lib/client/Usage.d.ts +7 -0
  34. package/lib/client/Usage.js +83 -0
  35. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  36. package/lib/client/VideoGenerateToolview.js +172 -0
  37. package/lib/client/context.d.ts +16 -0
  38. package/lib/client/context.js +11 -0
  39. package/lib/client/fast-command.d.ts +2 -0
  40. package/lib/client/fast-command.js +4 -0
  41. package/lib/client/hooks.d.ts +2 -0
  42. package/lib/client/hooks.js +10 -0
  43. package/lib/client/index.d.ts +3 -0
  44. package/lib/client/index.js +113 -0
  45. package/lib/client/locales.d.ts +139 -0
  46. package/lib/client/locales.js +267 -0
  47. package/lib/client/meter.d.ts +91 -0
  48. package/lib/client/meter.js +196 -0
  49. package/lib/client/prefs.d.ts +51 -0
  50. package/lib/client/prefs.js +71 -0
  51. package/lib/client/rpc.d.ts +161 -0
  52. package/lib/client/rpc.js +31 -0
  53. package/lib/client/store.d.ts +67 -0
  54. package/lib/client/store.js +129 -0
  55. package/lib/client/styles.d.ts +7 -0
  56. package/lib/client/styles.js +176 -0
  57. package/lib/client.js +3933 -0
  58. package/lib/client.js.map +1 -0
  59. package/lib/compat.d.ts +55 -0
  60. package/lib/compat.js +20 -0
  61. package/lib/http.d.ts +134 -0
  62. package/lib/http.js +431 -0
  63. package/lib/index.d.ts +121 -0
  64. package/lib/index.js +13210 -0
  65. package/lib/json-text.d.ts +2 -0
  66. package/lib/json-text.js +4 -0
  67. package/lib/model-defaults.d.ts +23 -0
  68. package/lib/model-defaults.js +238 -0
  69. package/lib/model-speeds.d.ts +10 -0
  70. package/lib/model-speeds.js +48 -0
  71. package/lib/models-directory.d.ts +17 -0
  72. package/lib/models-directory.js +25 -0
  73. package/lib/provider-settings.d.ts +55 -0
  74. package/lib/provider-settings.js +215 -0
  75. package/lib/providers/account-preferences.d.ts +46 -0
  76. package/lib/providers/account-preferences.js +188 -0
  77. package/lib/providers/accounts.d.ts +113 -0
  78. package/lib/providers/accounts.js +142 -0
  79. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  80. package/lib/providers/antigravity-oauth-client.js +7 -0
  81. package/lib/providers/antigravity.d.ts +119 -0
  82. package/lib/providers/antigravity.js +552 -0
  83. package/lib/providers/catalog-refresh.d.ts +4 -0
  84. package/lib/providers/catalog-refresh.js +30 -0
  85. package/lib/providers/catalog-store.d.ts +47 -0
  86. package/lib/providers/catalog-store.js +283 -0
  87. package/lib/providers/claude-cli-version.d.ts +15 -0
  88. package/lib/providers/claude-cli-version.js +24 -0
  89. package/lib/providers/claude.d.ts +212 -0
  90. package/lib/providers/claude.js +726 -0
  91. package/lib/providers/codex-client-version.d.ts +8 -0
  92. package/lib/providers/codex-client-version.js +17 -0
  93. package/lib/providers/codex-search.d.ts +29 -0
  94. package/lib/providers/codex-search.js +198 -0
  95. package/lib/providers/codex.d.ts +253 -0
  96. package/lib/providers/codex.js +930 -0
  97. package/lib/providers/common.d.ts +415 -0
  98. package/lib/providers/common.js +679 -0
  99. package/lib/providers/copilot.d.ts +365 -0
  100. package/lib/providers/copilot.js +956 -0
  101. package/lib/providers/grok.d.ts +214 -0
  102. package/lib/providers/grok.js +750 -0
  103. package/lib/providers/image-pool.d.ts +30 -0
  104. package/lib/providers/image-pool.js +89 -0
  105. package/lib/providers/npm-cli-version.d.ts +56 -0
  106. package/lib/providers/npm-cli-version.js +98 -0
  107. package/lib/providers/pool-family.d.ts +56 -0
  108. package/lib/providers/pool-family.js +45 -0
  109. package/lib/providers/pool-health.d.ts +91 -0
  110. package/lib/providers/pool-health.js +164 -0
  111. package/lib/providers/pool-usage.d.ts +106 -0
  112. package/lib/providers/pool-usage.js +302 -0
  113. package/lib/providers/pool.d.ts +115 -0
  114. package/lib/providers/pool.js +461 -0
  115. package/lib/providers/rate-limit.d.ts +192 -0
  116. package/lib/providers/rate-limit.js +338 -0
  117. package/lib/providers/replay.d.ts +4 -0
  118. package/lib/providers/replay.js +37 -0
  119. package/lib/providers/reset-redemption.d.ts +20 -0
  120. package/lib/providers/reset-redemption.js +70 -0
  121. package/lib/tools/image-generate.d.ts +128 -0
  122. package/lib/tools/image-generate.js +438 -0
  123. package/lib/tools/registration.d.ts +15 -0
  124. package/lib/tools/registration.js +24 -0
  125. package/lib/tools/video-generate.d.ts +89 -0
  126. package/lib/tools/video-generate.js +257 -0
  127. package/lib/tools/x-search.d.ts +58 -0
  128. package/lib/tools/x-search.js +197 -0
  129. package/lib/translate/anthropic.d.ts +180 -0
  130. package/lib/translate/anthropic.js +534 -0
  131. package/lib/translate/antigravity-images.d.ts +11 -0
  132. package/lib/translate/antigravity-images.js +62 -0
  133. package/lib/translate/antigravity-schema.d.ts +4 -0
  134. package/lib/translate/antigravity-schema.js +145 -0
  135. package/lib/translate/antigravity-thinking.d.ts +5 -0
  136. package/lib/translate/antigravity-thinking.js +44 -0
  137. package/lib/translate/antigravity.d.ts +120 -0
  138. package/lib/translate/antigravity.js +412 -0
  139. package/lib/translate/chat-completions.d.ts +120 -0
  140. package/lib/translate/chat-completions.js +382 -0
  141. package/lib/translate/resolved.d.ts +98 -0
  142. package/lib/translate/resolved.js +186 -0
  143. package/lib/translate/responses.d.ts +192 -0
  144. package/lib/translate/responses.js +443 -0
  145. package/lib/translate/sse.d.ts +21 -0
  146. package/lib/translate/sse.js +62 -0
  147. package/lib/usage/headers.d.ts +21 -0
  148. package/lib/usage/headers.js +64 -0
  149. package/lib/usage/pace.d.ts +53 -0
  150. package/lib/usage/pace.js +48 -0
  151. package/lib/usage/service.d.ts +64 -0
  152. package/lib/usage/service.js +104 -0
  153. package/lib/usage-providers/common.d.ts +23 -0
  154. package/lib/usage-providers/common.js +28 -0
  155. package/lib/usage-providers/index.d.ts +42 -0
  156. package/lib/usage-providers/index.js +71 -0
  157. package/lib/usage-providers/keys.d.ts +11 -0
  158. package/lib/usage-providers/keys.js +52 -0
  159. package/lib/usage-providers/kimi.d.ts +4 -0
  160. package/lib/usage-providers/kimi.js +52 -0
  161. package/lib/usage-providers/ollama-cloud.d.ts +4 -0
  162. package/lib/usage-providers/ollama-cloud.js +42 -0
  163. package/lib/usage-providers/zai.d.ts +5 -0
  164. package/lib/usage-providers/zai.js +53 -0
  165. package/locale/en.json +6 -0
  166. package/locale/zh.json +6 -0
  167. package/package.json +115 -4
@@ -0,0 +1,214 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Codex Speed toggle: one small control in the composer's right tool row
4
+ * (`conversation.input.right`), switching the session between standard routing
5
+ * and the fast (priority) service tier — the Codex desktop app's Speed menu.
6
+ * The choice is per session and lives in the node half (in-memory); this
7
+ * component holds only viewing state. The control renders nothing until the
8
+ * first load proves the session's current model is a codex model whose catalog
9
+ * advertises the fast tier.
10
+ *
11
+ * Every color resolves through a `--dsw-alias-*` design token and every
12
+ * user-visible string goes through the locale `t` of the
13
+ * 'subscription-models' namespace, same as the settings section.
14
+ */
15
+ import { useEffect, useRef, useState } from 'react';
16
+ import { call } from './rpc.js';
17
+ import { en } from './locales.js';
18
+ /**
19
+ * The `loadSpeed` half of the inject face: the plugin's own speed state plus
20
+ * the host's current model selection (the visibility gate). A model-RPC
21
+ * failure throws rather than answering "hidden" — the caller keeps its last
22
+ * known state, so a transient failure never locks the toggle away.
23
+ *
24
+ * `sessionId` is a plain string: slot and command contexts brand it through
25
+ * different dsh-session copies, and only the service boundary needs one.
26
+ *
27
+ * `models` resolves lazily per call: the ui-model-selection service may
28
+ * register after this plugin applies, and a shell without it (no model seat
29
+ * at all) simply keeps the toggle hidden.
30
+ */
31
+ export function createSpeedLoader(connection, models, sessionId) {
32
+ return async () => {
33
+ const directories = models();
34
+ const directory = directories?.directoryFor(sessionId);
35
+ const current = (await directory?.load())?.current ?? null;
36
+ const state = await call(connection.rpc, 'speed', {
37
+ sessionId, ...(current?.provider === 'codex' ? { model: current.model } : {}),
38
+ });
39
+ const visible = current !== null && current.provider === 'codex'
40
+ && state.fastModels.includes(current.model);
41
+ // Selection may change while the speed RPC is pending.
42
+ const latest = (await directory?.load())?.current ?? null;
43
+ if (latest?.provider !== current?.provider || latest?.model !== current?.model) {
44
+ throw new Error('Model changed while loading speed');
45
+ }
46
+ return { visible, tier: state.tier, ...(visible && current ? { model: current.model } : {}) };
47
+ };
48
+ }
49
+ /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
50
+ export function createSpeedSetter(connection, sessionId, models) {
51
+ return async (tier, displayedModel) => {
52
+ try {
53
+ const directories = models?.();
54
+ const current = directories === undefined ? null : (await directories.directoryFor(sessionId).load()).current;
55
+ if (models !== undefined && current?.provider !== 'codex')
56
+ return false;
57
+ if (displayedModel !== undefined && current?.model !== displayedModel)
58
+ return false;
59
+ await call(connection.rpc, 'setSpeed', {
60
+ sessionId, tier, ...(current?.provider === 'codex' ? { model: current.model } : {}),
61
+ });
62
+ return true;
63
+ }
64
+ catch {
65
+ return false; // The control keeps its last confirmed value on load or save failure.
66
+ }
67
+ };
68
+ }
69
+ /** English-dictionary fallback for a missing inject `t` (standalone renders). */
70
+ function fallbackTranslate(key) {
71
+ return en[key];
72
+ }
73
+ const TIERS = ['standard', 'fast'];
74
+ /**
75
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
76
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
77
+ * the current tier). Mount and every open reload the host state so a model
78
+ * switch made since the last open self-corrects.
79
+ */
80
+ /** How often the control re-reads the host state (model switches arrive only by asking). */
81
+ const POLL_INTERVAL_MS = 3000;
82
+ /**
83
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
84
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
85
+ * the current tier). The host pushes nothing on a model switch, so the
86
+ * control re-reads on a slow poll with a single-flight guard; a failed read
87
+ * keeps the last known state, so a transient RPC failure can never lock the
88
+ * toggle away (the earlier mount-only load had no recovery path).
89
+ */
90
+ export function SpeedSelect({ loadSpeed, setSpeed, t }) {
91
+ const translate = t ?? fallbackTranslate;
92
+ const [state, setState] = useState(null);
93
+ const [open, setOpen] = useState(false);
94
+ const [busy, setBusy] = useState(false);
95
+ const rootRef = useRef(null);
96
+ // The inject face may be re-evaluated (new callback identities) on re-render;
97
+ // the poll effect mounts once and reads through this ref, so identity churn
98
+ // neither resets the interval nor multiplies in-flight loads.
99
+ const loadRef = useRef(loadSpeed);
100
+ loadRef.current = loadSpeed;
101
+ const generation = useRef(0);
102
+ const mounted = useRef(false);
103
+ const refresh = async () => {
104
+ if (!mounted.current)
105
+ return;
106
+ const id = ++generation.current;
107
+ try {
108
+ const loaded = await loadRef.current?.();
109
+ if (loaded && id === generation.current)
110
+ setState(loaded);
111
+ }
112
+ catch { /* preserve the last confirmed state; a later poll can retry */ }
113
+ };
114
+ useEffect(() => {
115
+ if (loadRef.current === undefined)
116
+ return;
117
+ mounted.current = true;
118
+ let cancelled = false;
119
+ let inflight = false;
120
+ const reload = () => {
121
+ const load = loadRef.current;
122
+ if (load === undefined || inflight || cancelled)
123
+ return;
124
+ inflight = true;
125
+ void refresh().finally(() => { inflight = false; });
126
+ };
127
+ reload();
128
+ const timer = setInterval(reload, POLL_INTERVAL_MS);
129
+ return () => {
130
+ mounted.current = false;
131
+ ++generation.current;
132
+ cancelled = true;
133
+ clearInterval(timer);
134
+ };
135
+ }, []);
136
+ useEffect(() => {
137
+ if (!open)
138
+ return;
139
+ const closeOutside = (event) => {
140
+ if (!rootRef.current?.contains(event.target))
141
+ setOpen(false);
142
+ };
143
+ document.addEventListener('mousedown', closeOutside);
144
+ return () => { document.removeEventListener('mousedown', closeOutside); };
145
+ }, [open]);
146
+ if (loadSpeed === undefined || setSpeed === undefined || state === null || !state.visible) {
147
+ return null;
148
+ }
149
+ const choose = (tier) => {
150
+ if (busy)
151
+ return;
152
+ if (tier === state.tier) {
153
+ setOpen(false);
154
+ return;
155
+ }
156
+ setBusy(true);
157
+ void setSpeed(tier, state.model).then((ok) => {
158
+ if (!mounted.current)
159
+ return;
160
+ setBusy(false);
161
+ if (ok)
162
+ setOpen(false);
163
+ void refresh(); // Read the currently selected model, even when the save concerned another.
164
+ }, () => { if (mounted.current) {
165
+ setBusy(false);
166
+ void refresh();
167
+ } });
168
+ };
169
+ const show = () => {
170
+ setOpen(true);
171
+ void refresh();
172
+ };
173
+ const tierName = (tier) => translate(tier === 'fast' ? 'speedFast' : 'speedStandard');
174
+ const tierDescription = (tier) => translate(tier === 'fast' ? 'speedFastDescription' : 'speedStandardDescription');
175
+ const triggerLabel = `${translate('speed')} · ${tierName(state.tier)}`;
176
+ return (_jsxs("div", { ref: rootRef, className: "dsh-subscription-speed", style: styles.root, onKeyDown: (event) => {
177
+ if (event.key === 'Escape' && open) {
178
+ event.preventDefault();
179
+ setOpen(false);
180
+ }
181
+ }, children: [open && (_jsx("div", { style: styles.menu, role: "menu", "aria-label": translate('speed'), children: TIERS.map(tier => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": tier === state.tier, style: styles.item, disabled: busy, onClick: () => { choose(tier); }, children: [_jsx("span", { style: styles.itemCheck, children: tier === state.tier ? '✓' : '' }), _jsxs("span", { style: styles.itemText, children: [_jsx("span", { style: styles.itemName, children: tierName(tier) }), _jsx("span", { style: styles.itemDescription, children: tierDescription(tier) })] })] }, tier))) })), _jsx("button", { type: "button", style: styles.trigger, "aria-haspopup": "menu", "aria-expanded": open, title: triggerLabel, disabled: busy, onClick: () => {
182
+ if (open)
183
+ setOpen(false);
184
+ else
185
+ show();
186
+ }, children: triggerLabel })] }));
187
+ }
188
+ const styles = {
189
+ root: { position: 'relative', display: 'inline-flex' },
190
+ trigger: {
191
+ border: '0.5px solid var(--dsw-alias-border-l3)', borderRadius: 'var(--dsw-radius-sm, 8px)',
192
+ background: 'var(--dsh-subscription-speed-hover, transparent)', color: 'var(--dsw-alias-label-secondary)',
193
+ font: 'inherit', fontSize: 12, lineHeight: '18px',
194
+ padding: '2px 8px', cursor: 'pointer', whiteSpace: 'nowrap',
195
+ },
196
+ menu: {
197
+ position: 'absolute', bottom: '100%', right: 0, marginBottom: 4,
198
+ minWidth: 180, padding: 4, zIndex: 20,
199
+ background: 'var(--dsw-specific-menu, var(--dsw-alias-bg-layer-2))', border: 0,
200
+ boxShadow: 'var(--dsw-elevation-prominent)', borderRadius: 'var(--dsw-radius-lg, 16px)', display: 'flex', flexDirection: 'column', gap: 2,
201
+ },
202
+ item: {
203
+ display: 'flex', alignItems: 'flex-start', gap: 6, width: '100%',
204
+ border: 'none', borderRadius: 'var(--dsw-radius-sm, 8px)', background: 'var(--dsh-subscription-speed-hover, transparent)',
205
+ padding: '6px 8px', cursor: 'pointer', font: 'inherit', textAlign: 'left',
206
+ },
207
+ itemCheck: {
208
+ width: 14, flexShrink: 0, fontSize: 12, lineHeight: '18px',
209
+ color: 'var(--dsw-alias-label-primary)',
210
+ },
211
+ itemText: { display: 'flex', flexDirection: 'column' },
212
+ itemName: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-primary)' },
213
+ itemDescription: { fontSize: 11, lineHeight: '16px', color: 'var(--dsw-alias-label-tertiary)' },
214
+ };
@@ -0,0 +1,7 @@
1
+ import type { SurfaceInjected } from './context.js';
2
+ /** The Usage row's icon in the settings nav; the shell passes the edge size. */
3
+ export declare function UsageNavIcon({ size }: {
4
+ size: number;
5
+ }): import("react").JSX.Element;
6
+ export type UsageProps = Partial<SurfaceInjected>;
7
+ export declare function Usage(props: UsageProps): import("react").JSX.Element | null;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The Usage settings page (`settings.section`): one block per signed-in
4
+ * provider that reports usage, plus the display options. Accounts and models
5
+ * stay on the Models page.
6
+ */
7
+ import { useState } from 'react';
8
+ import { call, KEY_PROVIDERS } from './rpc.js';
9
+ import { splitAccounts, usageKey } from './store.js';
10
+ import { Account } from './ProviderCard.js';
11
+ import { Icon, tf } from './meter.js';
12
+ import { useNow } from './hooks.js';
13
+ import { usePrefs, useStore } from './context.js';
14
+ import { isMetered, USAGE_PROVIDERS, shortName } from './rpc.js';
15
+ /** The Usage row's icon in the settings nav; the shell passes the edge size. */
16
+ export function UsageNavIcon({ size }) {
17
+ return _jsx(Icon, { name: "meter", size: size });
18
+ }
19
+ export function Usage(props) {
20
+ const { store, prefs, rpc, t } = props;
21
+ if (store === undefined || prefs === undefined || rpc === undefined || t === undefined)
22
+ return null;
23
+ return _jsx(UsageBody, { store: store, prefs: prefs, rpc: rpc, t: t });
24
+ }
25
+ function Segmented({ label, value, options, disabled, onChange }) {
26
+ return (_jsx("span", { className: "dsm-seg", role: "group", "aria-label": label, children: options.map(([option, text]) => (_jsx("button", { type: "button", "aria-pressed": value === option, disabled: disabled, onClick: () => { onChange(option); }, children: text }, option))) }));
27
+ }
28
+ function maxManualReady(snapshot, provider, status) {
29
+ const times = status.accounts.map(a => {
30
+ const entry = snapshot.usage.get(usageKey(provider, a.key));
31
+ return entry?.kind === 'ready' ? entry.usage.manualReadyAt : undefined;
32
+ }).filter(at => at !== undefined);
33
+ return times.length === 0 ? undefined : Math.max(...times);
34
+ }
35
+ function UsageBody({ store, prefs, rpc, t }) {
36
+ const snapshot = useStore(store);
37
+ const view = usePrefs(prefs);
38
+ const now = useNow(30_000);
39
+ const readOnly = !view.writable;
40
+ const providers = USAGE_PROVIDERS.filter(provider => {
41
+ const status = snapshot.status?.providers[provider];
42
+ return status !== undefined && status.accounts.length > 0 && isMetered(snapshot.status, provider);
43
+ });
44
+ return (_jsxs("section", { className: "dsm-footer-block dsm-usage", "aria-labelledby": "dsm-usage-title", style: { marginTop: 0, borderTop: 'none', paddingTop: 0 }, children: [_jsx("h3", { id: "dsm-usage-title", children: t('usage') }), _jsxs("div", { className: "dsm-row", children: [_jsx("span", { className: "dsm-grow", children: t('showQuotaAs') }), _jsx(Segmented, { label: t('showQuotaAs'), value: view.display, disabled: readOnly, options: [['left', t('displayLeft')], ['used', t('displayUsed')]], onChange: display => { void prefs.set({ display }); } })] }), _jsxs("div", { className: "dsm-row", children: [_jsx("span", { className: "dsm-grow", children: t('paceWarnings') }), _jsx("button", { type: "button", role: "switch", className: "dsm-switch", "aria-checked": view.paceWarnings, "aria-label": t('paceWarnings'), disabled: readOnly, onClick: () => { void prefs.set({ paceWarnings: !view.paceWarnings }); } })] }), _jsxs("div", { className: "dsm-row", children: [_jsxs("span", { className: "dsm-grow", children: [t('composerUsage'), " ", _jsx("span", { className: "dsm-note", children: t('composerUsageNote') })] }), _jsx("button", { type: "button", role: "switch", className: "dsm-switch", "aria-checked": view.composer, "aria-label": t('composerUsage'), disabled: readOnly, onClick: () => { void prefs.set({ composer: !view.composer }); } })] }), providers.length === 0 ? _jsx("p", { className: "dsm-muted", style: { margin: '12px 0 0' }, children: t('usageEmpty') }) : null, providers.map(provider => {
45
+ const status = snapshot.status?.providers[provider];
46
+ const split = status === undefined ? undefined : splitAccounts(status);
47
+ if (status === undefined || split === undefined)
48
+ return null;
49
+ const pinned = view.pinned.includes(provider);
50
+ const pinLabel = pinned ? t('unpin') : t('pin');
51
+ const refreshing = status.accounts.some(a => snapshot.refreshing.has(usageKey(provider, a.key)));
52
+ const readyAt = maxManualReady(snapshot, provider, status);
53
+ const cooling = readyAt !== undefined && readyAt > now;
54
+ return (_jsxs("div", { className: "dsm-usage-block", style: { borderTop: '.5px solid var(--dsw-alias-border-l1)', padding: '10px 0' }, children: [_jsxs("div", { className: "dsm-row", style: { minHeight: 28 }, children: [_jsx("span", { className: "dsm-grow", style: { fontWeight: 500 }, children: shortName(snapshot.status, provider) }), _jsx("button", { type: "button", className: "dsm-btn dsm-icon dsm-pin", "aria-pressed": pinned, title: pinLabel, "aria-label": pinLabel, disabled: readOnly, onClick: () => { void prefs.togglePinned(provider); }, children: _jsx(Icon, { name: pinned ? 'pinFilled' : 'pin' }) }), isKey(provider) ? (_jsx("button", { type: "button", className: "dsm-btn dsm-ghost", onClick: () => { void call(rpc, 'keysRemove', { provider }).then(() => store.refreshStatus()); }, children: t('removeKey') })) : null, _jsx("button", { type: "button", className: `dsm-btn dsm-icon${refreshing ? ' dsm-spinning' : ''}`, disabled: refreshing || cooling, title: cooling ? tf(t, 'refreshedWait', { seconds: Math.ceil((readyAt - now) / 1000) }) : t('refreshUsage'), "aria-label": tf(t, 'refreshUsageNamed', { name: provider }), onClick: () => { void store.refreshUsage('manual', provider); }, children: _jsx(Icon, { name: "refresh" }) })] }), _jsx(Account, { t: t, provider: provider, account: split.active, status: status, snapshot: snapshot, prefs: view, now: now, compact: true }), split.rest.map(account => _jsx(Account, { t: t, provider: provider, account: account, status: status, snapshot: snapshot, prefs: view, now: now, compact: true }, account.key))] }, provider));
55
+ }), _jsx(AddProvider, { t: t, store: store, rpc: rpc, unconfigured: KEY_PROVIDERS.filter(id => snapshot.status?.keyProviders?.find(v => v.id === id)?.configured !== true), names: snapshot.status?.keyProviders ?? [] })] }));
56
+ }
57
+ const isKey = (provider) => KEY_PROVIDERS.includes(provider);
58
+ function AddProvider({ t, store, rpc, unconfigured, names }) {
59
+ const [picked, setPicked] = useState();
60
+ const [key, setKey] = useState('');
61
+ const [busy, setBusy] = useState(false);
62
+ const [error, setError] = useState();
63
+ if (unconfigured.length === 0)
64
+ return null;
65
+ const save = async (id) => {
66
+ setBusy(true);
67
+ setError(undefined);
68
+ try {
69
+ await call(rpc, 'keysSet', { provider: id, key });
70
+ setKey('');
71
+ setPicked(undefined);
72
+ await store.refreshStatus();
73
+ }
74
+ catch (failure) {
75
+ setError(failure instanceof Error ? failure.message : String(failure));
76
+ }
77
+ finally {
78
+ setBusy(false);
79
+ }
80
+ };
81
+ const nameOf = (id) => names.find(v => v.id === id)?.name ?? id;
82
+ return (_jsxs("div", { className: "dsm-usage-block", style: { borderTop: '.5px solid var(--dsw-alias-border-l1)', padding: '10px 0' }, children: [_jsxs("div", { className: "dsm-row", style: { minHeight: 28 }, children: [_jsx("span", { className: "dsm-grow dsm-muted", children: t('addProvider') }), unconfigured.map(id => (_jsx("button", { type: "button", className: "dsm-btn", "aria-pressed": picked === id, onClick: () => { setPicked(picked === id ? undefined : id); setError(undefined); }, children: nameOf(id) }, id)))] }), picked === undefined ? null : (_jsxs("div", { className: "dsm-panel", style: { paddingTop: 8 }, children: [_jsx("input", { className: "dsm-input", type: "password", autoComplete: "off", value: key, placeholder: t('apiKey'), "aria-label": `${nameOf(picked)} ${t('apiKey')}`, onChange: event => { setKey(event.target.value); } }), _jsxs("div", { className: "dsm-panel-foot", children: [_jsx("span", { className: "dsm-note", role: "status", style: { marginRight: 'auto' }, children: busy ? t('keyChecking') : error ?? t('keyHelp') }), _jsx("button", { type: "button", className: "dsm-btn dsm-primary", disabled: busy || key.trim() === '', onClick: () => { void save(picked); }, children: t('addKey') })] })] }))] }));
83
+ }
@@ -0,0 +1,42 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ import type { SubscriptionsKey } from './locales.js';
4
+ /** Mirror of ui-tool's ToolCallOwnerProps (see ImageGenerateToolview). */
5
+ interface ToolCallOwnerProps {
6
+ callId: string;
7
+ toolName: string;
8
+ block: ToolCallBlock;
9
+ cwd?: string | undefined;
10
+ openFile: (path: string) => void;
11
+ inspect?: (() => void) | undefined;
12
+ }
13
+ /** Decoded video bytes as the node half's `video` endpoint answers them. */
14
+ export interface VideoBytes {
15
+ mediaType: string;
16
+ dataBase64: string;
17
+ }
18
+ /** Injected dependencies of {@link VideoGenerateToolview} (slot `inject`). */
19
+ export interface VideoGenerateToolviewInjected {
20
+ /** Loads one generated video's bytes by bare file name. */
21
+ loadVideo: (name: string) => Promise<VideoBytes>;
22
+ }
23
+ /**
24
+ * Props delivered by the toolview outlet: the owner share plus the inject
25
+ * face and the framework locale seat, spread flat.
26
+ */
27
+ export type VideoGenerateToolviewProps = Partial<ToolCallOwnerProps> & Partial<VideoGenerateToolviewInjected> & {
28
+ t?: ((key: SubscriptionsKey, params?: Record<string, unknown>) => string) | undefined;
29
+ };
30
+ /**
31
+ * Build the video loader over the `/subscriptions-auth` `video` endpoint.
32
+ * @param rpc - Connection RPC caller.
33
+ * @returns loader resolving a bare file name to the decoded bytes.
34
+ */
35
+ export declare function createVideoLoader(rpc: ConnectionHandle['rpc']): (name: string) => Promise<VideoBytes>;
36
+ /**
37
+ * The `video_generate` keyed toolview component.
38
+ * @param props - owner share, inject face, and locale seat (spread flat).
39
+ * @returns the call row plus, once settled, the player / text / error body.
40
+ */
41
+ export declare function VideoGenerateToolview(props: VideoGenerateToolviewProps): import("react").JSX.Element | null;
42
+ export {};
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Keyed toolview for the `video_generate` tool: renders the generated video
4
+ * inline in the conversation. The row shows the call's prompt while running
5
+ * and after settling; a settled success loads the MP4 bytes through the node
6
+ * half's `/subscriptions-auth` `video` endpoint (by bare file name), builds a
7
+ * Blob URL, and plays it in a native `<video controls>` element. The file
8
+ * name comes from the result's presentation meta when the dispatch was
9
+ * top-level, and is recovered from the "Saved video to …" text line for
10
+ * nested (Code Mode) dispatches, where the harness computes no meta. A result
11
+ * with neither renders its text; an error result renders the first error line.
12
+ *
13
+ * The 'tool.call.toolview' SlotMap entry is declared by
14
+ * ImageGenerateToolview.tsx in this same package (one declaration per
15
+ * augmentation), so this file only mirrors the owner-props shape.
16
+ */
17
+ import { useEffect, useState } from 'react';
18
+ import { Icon } from './meter.js';
19
+ import { call } from './rpc.js';
20
+ import { en } from './locales.js';
21
+ /** Title prompt truncation budget (characters). */
22
+ const PROMPT_MAX_LENGTH = 60;
23
+ /**
24
+ * Build the video loader over the `/subscriptions-auth` `video` endpoint.
25
+ * @param rpc - Connection RPC caller.
26
+ * @returns loader resolving a bare file name to the decoded bytes.
27
+ */
28
+ export function createVideoLoader(rpc) {
29
+ return name => call(rpc, 'video', { name });
30
+ }
31
+ /**
32
+ * English-dictionary fallback for a missing locale seat (standalone renders);
33
+ * the framework always supplies the namespace-bound one.
34
+ */
35
+ function fallbackTranslate(key, params) {
36
+ let text = en[key];
37
+ for (const [name, value] of Object.entries(params ?? {})) {
38
+ text = text.replaceAll(`{${name}}`, String(value));
39
+ }
40
+ return text;
41
+ }
42
+ /** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
43
+ function derivePrompt(argsRaw) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(argsRaw);
47
+ }
48
+ catch {
49
+ // Non-JSON args (mid-stream truncation): fall back to the raw string below.
50
+ parsed = undefined;
51
+ }
52
+ let prompt;
53
+ if (typeof parsed === 'object' && parsed !== null) {
54
+ const args = parsed;
55
+ if (typeof args.prompt === 'string' && args.prompt !== '')
56
+ prompt = args.prompt;
57
+ else {
58
+ for (const value of Object.values(args)) {
59
+ if (typeof value === 'string' && value !== '') {
60
+ prompt = value;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ }
66
+ const line = (prompt ?? argsRaw).split('\n', 1)[0] ?? '';
67
+ return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
68
+ }
69
+ /** Flatten a settled result's text blocks (the fallback body and the error line). */
70
+ function resultText(block) {
71
+ if (!('kind' in block))
72
+ return '';
73
+ const parts = [];
74
+ for (const part of block.content) {
75
+ if (part.type === 'text')
76
+ parts.push(part.text);
77
+ }
78
+ if (parts.length === 0 && block.error !== undefined)
79
+ parts.push(`${block.error.name}: ${block.error.code}`);
80
+ return parts.join('\n');
81
+ }
82
+ /**
83
+ * The generated video's bare file name: presentation meta first (top-level
84
+ * dispatches), then the render text's "Saved video to …" line (nested
85
+ * dispatches compute no meta).
86
+ */
87
+ function resolveFileName(block) {
88
+ if (!('kind' in block))
89
+ return undefined;
90
+ const meta = block.meta;
91
+ if (typeof meta === 'object' && meta !== null) {
92
+ const fileName = meta.fileName;
93
+ if (typeof fileName === 'string' && fileName.length > 0)
94
+ return fileName;
95
+ }
96
+ const match = /^Saved video to (.+\.mp4)/m.exec(resultText(block));
97
+ if (match === null)
98
+ return undefined;
99
+ const path = match[1];
100
+ return path.slice(path.lastIndexOf('/') + 1);
101
+ }
102
+ /** Decode a base64 payload into bytes (browser-side; no Buffer). */
103
+ function base64Bytes(dataBase64) {
104
+ const binary = atob(dataBase64);
105
+ const bytes = new Uint8Array(binary.length);
106
+ for (let index = 0; index < binary.length; index++)
107
+ bytes[index] = binary.charCodeAt(index);
108
+ return bytes;
109
+ }
110
+ const styles = {
111
+ container: { display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0' },
112
+ row: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
113
+ icon: { display: 'inline-flex', flexShrink: 0, color: 'var(--dsw-alias-label-tertiary)' },
114
+ title: {
115
+ fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)',
116
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
117
+ },
118
+ subtle: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
119
+ output: {
120
+ margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)',
121
+ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
122
+ },
123
+ error: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
124
+ video: {
125
+ display: 'block', maxWidth: 480, width: '100%', borderRadius: 8,
126
+ backgroundColor: 'var(--dsw-alias-fill-tertiary)',
127
+ },
128
+ };
129
+ /**
130
+ * The `video_generate` keyed toolview component.
131
+ * @param props - owner share, inject face, and locale seat (spread flat).
132
+ * @returns the call row plus, once settled, the player / text / error body.
133
+ */
134
+ export function VideoGenerateToolview(props) {
135
+ const { block, loadVideo } = props;
136
+ const t = props.t ?? fallbackTranslate;
137
+ const settled = block !== undefined && 'kind' in block;
138
+ const isError = settled && block.isError;
139
+ const fileName = block !== undefined && settled && !isError ? resolveFileName(block) : undefined;
140
+ const [load, setLoad] = useState({ phase: 'loading' });
141
+ useEffect(() => {
142
+ if (fileName === undefined || loadVideo === undefined)
143
+ return;
144
+ let cancelled = false;
145
+ let objectUrl;
146
+ setLoad({ phase: 'loading' });
147
+ loadVideo(fileName).then((video) => {
148
+ if (cancelled)
149
+ return;
150
+ // Uint8Array#slice detaches a plain ArrayBuffer view for BlobPart.
151
+ objectUrl = URL.createObjectURL(new Blob([base64Bytes(video.dataBase64).slice()], { type: video.mediaType }));
152
+ setLoad({ phase: 'ready', url: objectUrl });
153
+ }, (error) => {
154
+ if (cancelled)
155
+ return;
156
+ setLoad({ phase: 'failed', message: error instanceof Error ? error.message : String(error) });
157
+ });
158
+ return () => {
159
+ cancelled = true;
160
+ if (objectUrl !== undefined)
161
+ URL.revokeObjectURL(objectUrl);
162
+ };
163
+ }, [fileName, loadVideo]);
164
+ if (block === undefined)
165
+ return null;
166
+ const argsRaw = (settled ? block.call?.argsRaw : 'argsRaw' in block ? block.argsRaw : undefined) ?? '';
167
+ const title = `video_generate: ${derivePrompt(argsRaw)}`;
168
+ const text = settled ? resultText(block) : '';
169
+ return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.row, children: [_jsx("span", { style: styles.icon, children: _jsx(Icon, { name: "sparkle", size: 14 }) }), _jsx("span", { style: styles.title, children: title })] }), !settled && _jsx("p", { style: styles.subtle, children: t('generatingVideo') }), settled && isError && text !== '' && (_jsx("p", { style: styles.error, children: text.split('\n', 1)[0] })), settled && !isError && fileName !== undefined && load.phase === 'loading' && (_jsx("p", { style: styles.subtle, children: t('videoLoading') })), settled && !isError && fileName !== undefined && load.phase === 'failed' && (_jsx("p", { style: styles.error, children: t('videoLoadFailed', { message: load.message }) })), settled && !isError && fileName !== undefined && load.phase === 'ready' && (
170
+ // eslint-disable-next-line jsx-a11y/media-has-caption -- generated clip; no caption track exists.
171
+ _jsx("video", { style: styles.video, src: load.url, controls: true, preload: "metadata" })), settled && !isError && fileName === undefined && text !== '' && (_jsx("p", { style: styles.output, children: text }))] }));
172
+ }
@@ -0,0 +1,16 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { ProviderId, UsagePreferencesView } from './prefs.js';
3
+ import type { SubscriptionsStore, StoreSnapshot } from './store.js';
4
+ import type { T } from './meter.js';
5
+ import type { PrefsStore } from './prefs.js';
6
+ export interface SurfaceInjected {
7
+ store: SubscriptionsStore;
8
+ prefs: PrefsStore;
9
+ rpc: ConnectionHandle['rpc'];
10
+ t: T;
11
+ /** Open Settings at the Models page, when the shell offers it. */
12
+ openSettings?: () => void;
13
+ }
14
+ export declare function useStore(store: SubscriptionsStore): StoreSnapshot;
15
+ export declare function usePrefs(prefs: PrefsStore): UsagePreferencesView;
16
+ export type { ProviderId };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * What every surface receives through slot `inject`: the shared store, the
3
+ * RPC caller, the copy function, and the usage preferences form.
4
+ */
5
+ import { useSyncExternalStore } from 'react';
6
+ export function useStore(store) {
7
+ return useSyncExternalStore(store.subscribe, store.getSnapshot);
8
+ }
9
+ export function usePrefs(prefs) {
10
+ return useSyncExternalStore(prefs.subscribe, prefs.getSnapshot);
11
+ }
@@ -0,0 +1,2 @@
1
+ /** Build the locale resolver used by the /fast command contribution. */
2
+ export declare function fastCommandDescription(t: () => string): () => string;
@@ -0,0 +1,4 @@
1
+ /** Build the locale resolver used by the /fast command contribution. */
2
+ export function fastCommandDescription(t) {
3
+ return () => t();
4
+ }
@@ -0,0 +1,2 @@
1
+ /** The current time, re-read every `intervalMs`, so countdowns advance without a fetch. */
2
+ export declare function useNow(intervalMs: number): number;
@@ -0,0 +1,10 @@
1
+ import { useEffect, useState } from 'react';
2
+ /** The current time, re-read every `intervalMs`, so countdowns advance without a fetch. */
3
+ export function useNow(intervalMs) {
4
+ const [now, setNow] = useState(() => Date.now());
5
+ useEffect(() => {
6
+ const timer = setInterval(() => { setNow(Date.now()); }, intervalMs);
7
+ return () => { clearInterval(timer); };
8
+ }, [intervalMs]);
9
+ return now;
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ export declare const inject: string[];
3
+ export declare function apply(ctx: ClientContext): void;