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,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Plugin-wide options after the Models provider rows
4
+ * (`settings.models.footer`): the subscription-only proxy. Usage options live in the Usage page.
5
+ */
6
+ import { useEffect, useState } from 'react';
7
+ import { call } from './rpc.js';
8
+ import { tf } from './meter.js';
9
+ export function Footer(props) {
10
+ const { store, prefs, rpc, t } = props;
11
+ if (store === undefined || prefs === undefined || rpc === undefined || t === undefined)
12
+ return null;
13
+ return _jsx(FooterBody, { rpc: rpc, t: t });
14
+ }
15
+ function FooterBody({ rpc, t }) {
16
+ return (_jsxs("section", { className: "dsm-footer-block", "aria-labelledby": "dsm-footer-title", children: [_jsx("h3", { id: "dsm-footer-title", children: t('subscriptions') }), _jsx(ProxyRow, { t: t, rpc: rpc })] }));
17
+ }
18
+ function ProxyRow({ t, rpc }) {
19
+ const [config, setConfig] = useState();
20
+ const [open, setOpen] = useState(false);
21
+ const [url, setUrl] = useState('');
22
+ const [bypass, setBypass] = useState('');
23
+ const [enabled, setEnabled] = useState(false);
24
+ const [message, setMessage] = useState();
25
+ const [busy, setBusy] = useState(false);
26
+ useEffect(() => {
27
+ let live = true;
28
+ call(rpc, 'proxyGet', {}).then(value => { if (live)
29
+ setConfig(value); }, () => undefined);
30
+ return () => { live = false; };
31
+ }, [rpc]);
32
+ const edit = () => {
33
+ setUrl(config?.url ?? '');
34
+ setBypass((config?.bypass ?? []).join(', '));
35
+ setEnabled(config?.enabled ?? false);
36
+ setMessage(undefined);
37
+ setOpen(true);
38
+ };
39
+ const save = async () => {
40
+ setBusy(true);
41
+ setMessage(undefined);
42
+ try {
43
+ const saved = await call(rpc, 'proxySet', {
44
+ enabled, url: url.trim(), bypass: bypass.split(',').map(entry => entry.trim()).filter(entry => entry !== ''),
45
+ });
46
+ setConfig(saved);
47
+ setOpen(false);
48
+ }
49
+ catch (failure) {
50
+ setMessage(tf(t, 'saveFailed', { message: failure instanceof Error ? failure.message : String(failure) }));
51
+ }
52
+ finally {
53
+ setBusy(false);
54
+ }
55
+ };
56
+ const test = async () => {
57
+ setBusy(true);
58
+ setMessage(undefined);
59
+ try {
60
+ const result = await call(rpc, 'proxyTest', url.trim() === '' ? {} : { proxy: { url: url.trim() } });
61
+ setMessage(result.ok && result.status !== undefined
62
+ ? tf(t, 'proxyTestOk', { status: result.status, ms: result.latencyMs ?? 0 })
63
+ : result.error ?? '');
64
+ }
65
+ catch (failure) {
66
+ setMessage(failure instanceof Error ? failure.message : String(failure));
67
+ }
68
+ finally {
69
+ setBusy(false);
70
+ }
71
+ };
72
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "dsm-row", children: [_jsxs("span", { className: "dsm-grow", children: [t('proxy'), " ", _jsx("span", { className: "dsm-note", children: config?.enabled === true ? config.url : t('proxyOff') })] }), open ? null : _jsx("button", { type: "button", className: "dsm-btn", onClick: edit, children: t('configure') })] }), open ? (_jsxs("div", { className: "dsm-panel", style: { paddingTop: 12 }, children: [_jsxs("div", { className: "dsm-row", children: [_jsx("span", { className: "dsm-grow", children: t('proxyEnabled') }), _jsx("button", { type: "button", role: "switch", className: "dsm-switch", "aria-checked": enabled, "aria-label": t('proxyEnabled'), onClick: () => { setEnabled(value => !value); } })] }), _jsx("input", { className: "dsm-input", value: url, placeholder: "http://127.0.0.1:7890", "aria-label": t('proxyUrl'), onChange: event => { setUrl(event.target.value); } }), _jsx("input", { className: "dsm-input", value: bypass, placeholder: t('proxyBypass'), "aria-label": t('proxyBypass'), onChange: event => { setBypass(event.target.value); } }), _jsxs("div", { className: "dsm-panel-foot", children: [message === undefined ? null : _jsx("span", { className: "dsm-note", role: "status", style: { marginRight: 'auto' }, children: message }), _jsx("button", { type: "button", className: "dsm-btn dsm-ghost", disabled: busy, onClick: () => { void test(); }, children: t('proxyTest') }), _jsx("button", { type: "button", className: "dsm-btn dsm-ghost", onClick: () => { setOpen(false); }, children: t('cancel') }), _jsx("button", { type: "button", className: "dsm-btn dsm-primary", disabled: busy, onClick: () => { void save(); }, children: t('save') })] })] })) : null] }));
73
+ }
@@ -0,0 +1,28 @@
1
+ import type { UsageProviderId as ProviderId } from './rpc.js';
2
+ import type { StoreSnapshot } from './store.js';
3
+ import type { SurfaceInjected } from './context.js';
4
+ import type { UsagePreferencesView } from './prefs.js';
5
+ /** The session's model selection, as `modelDirectories` publishes it. */
6
+ export interface CurrentModelSource {
7
+ getSnapshot(): {
8
+ current: {
9
+ provider: string;
10
+ model: string;
11
+ } | null;
12
+ };
13
+ subscribe(listener: () => void): () => void;
14
+ }
15
+ export interface GaugeInjected extends SurfaceInjected {
16
+ sessionId: string;
17
+ /** Undefined when the model-selection service is not mounted. */
18
+ currentModel: CurrentModelSource | undefined;
19
+ }
20
+ /** Providers the gauge shows: the current model's provider first, then the pinned ones. */
21
+ export declare function gaugeProviders(prefs: UsagePreferencesView, current: string | undefined, snapshot: StoreSnapshot): ProviderId[];
22
+ /** Every provider the popover can list: the shown ones first, then the rest in page order. */
23
+ export declare function popoverProviders(shown: readonly ProviderId[], snapshot: StoreSnapshot): {
24
+ shown: ProviderId[];
25
+ others: ProviderId[];
26
+ };
27
+ export type GaugeProps = Partial<GaugeInjected>;
28
+ export declare function Gauge(props: GaugeProps): import("react").JSX.Element | null;
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Composer usage gauge and its popover. Rendered under the composer
4
+ * (`conversation.composer.dock`) once a conversation exists, and in the
5
+ * New Session row (`conversation.hero.options`) before the first prompt.
6
+ */
7
+ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
8
+ import { createPortal } from 'react-dom';
9
+ import { isMetered, isUsageProviderId, USAGE_PROVIDERS, shortName } from './rpc.js';
10
+ import { activeAccountKey, splitAccounts, usageKey } from './store.js';
11
+ import { Icon, Minis, tf } from './meter.js';
12
+ import { Account } from './ProviderCard.js';
13
+ import { useNow } from './hooks.js';
14
+ import { usePrefs, useStore } from './context.js';
15
+ const noSubscribe = () => () => undefined;
16
+ /** Select the provider string itself, so the snapshot compares by value. */
17
+ function useCurrentProvider(source) {
18
+ return useSyncExternalStore(source?.subscribe ?? noSubscribe, () => source?.getSnapshot().current?.provider);
19
+ }
20
+ /** A provider the composer can show: signed in and reporting usage. */
21
+ const showable = (snapshot, id) => isMetered(snapshot.status, id) && (snapshot.status?.providers[id]?.accounts.length ?? 0) > 0;
22
+ /** Providers the gauge shows: the current model's provider first, then the pinned ones. */
23
+ export function gaugeProviders(prefs, current, snapshot) {
24
+ if (snapshot.status === undefined)
25
+ return [];
26
+ const ids = new Set(isUsageProviderId(current) ? [current, ...prefs.pinned] : prefs.pinned);
27
+ return [...ids].filter(id => showable(snapshot, id));
28
+ }
29
+ /** Every provider the popover can list: the shown ones first, then the rest in page order. */
30
+ export function popoverProviders(shown, snapshot) {
31
+ return { shown: [...shown], others: USAGE_PROVIDERS.filter(id => !shown.includes(id) && showable(snapshot, id)) };
32
+ }
33
+ function windowsOf(snapshot, provider) {
34
+ const key = activeAccountKey(snapshot.status?.providers[provider]);
35
+ if (key === undefined)
36
+ return undefined;
37
+ const entry = snapshot.usage.get(usageKey(provider, key));
38
+ return entry?.kind === 'ready' ? entry.usage.windows : undefined;
39
+ }
40
+ export function Gauge(props) {
41
+ const { store, prefs, rpc, t, sessionId } = props;
42
+ if (store === undefined || prefs === undefined || rpc === undefined || t === undefined || sessionId === undefined)
43
+ return null;
44
+ return _jsx(GaugeBody, { ...props, store: store, prefs: prefs, rpc: rpc, t: t, sessionId: sessionId, currentModel: props.currentModel });
45
+ }
46
+ function GaugeBody({ store, prefs, t, sessionId, currentModel, openSettings }) {
47
+ const snapshot = useStore(store);
48
+ const prefView = usePrefs(prefs);
49
+ const current = useCurrentProvider(currentModel);
50
+ const now = useNow(30_000);
51
+ const [open, setOpen] = useState(false);
52
+ const anchor = useRef(null);
53
+ useEffect(() => { store.focusSession(sessionId); }, [store, sessionId]);
54
+ if (!prefView.composer || snapshot.status === undefined)
55
+ return null;
56
+ const shown = gaugeProviders(prefView, current, snapshot);
57
+ // Nothing to show and nothing to open: no subscription reports usage.
58
+ if (shown.length === 0 && popoverProviders(shown, snapshot).others.length === 0)
59
+ return null;
60
+ const toggle = () => {
61
+ setOpen(value => {
62
+ // Opening re-reads anything older than a minute; the Host enforces that bound.
63
+ if (!value)
64
+ void store.refreshUsage('open');
65
+ return !value;
66
+ });
67
+ };
68
+ return (_jsxs("span", { className: "dsm-gauge-slot", children: [_jsx("button", { ref: anchor, type: "button", className: "dsm-gauge", "aria-expanded": open, "aria-haspopup": "dialog", "aria-label": t('usageLabel'), title: shown.length === 0 ? t('usageLabel') : undefined, onClick: toggle, children: shown.length === 0 ? _jsx(Icon, { name: "meter" }) : shown.map(provider => (_jsxs("span", { className: "dsm-gp", children: [_jsx("span", { className: "dsm-gp-name", children: shortName(snapshot.status, provider) }), _jsx(Minis, { t: t, windows: windowsOf(snapshot, provider), display: prefView.display, now: now })] }, provider))) }), open ? createPortal(_jsx(Popover, { t: t, store: store, prefs: prefs, snapshot: snapshot, prefView: prefView, shown: shown, now: now, anchor: anchor, onClose: () => { setOpen(false); anchor.current?.focus(); }, ...openSettings === undefined ? {} : { onOpenSettings: () => { setOpen(false); openSettings(); } } }), document.body) : null] }));
69
+ }
70
+ function Popover({ t, store, prefs, snapshot, prefView, shown, now, anchor, onClose, onOpenSettings }) {
71
+ const panel = useRef(null);
72
+ const [position, setPosition] = useState();
73
+ const [openSections, setOpenSections] = useState(() => new Set(shown));
74
+ const [othersOpen, setOthersOpen] = useState(false);
75
+ const [openMore, setOpenMore] = useState(() => new Set());
76
+ const [manualAt, setManualAt] = useState();
77
+ // Above the anchor like the host's stat dialogs; below it when there is no room (the New Session row).
78
+ useLayoutEffect(() => {
79
+ const place = () => {
80
+ const a = anchor.current?.getBoundingClientRect();
81
+ const p = panel.current?.getBoundingClientRect();
82
+ if (a === undefined || p === undefined)
83
+ return;
84
+ const left = Math.min(Math.max(8, a.left + a.width / 2 - p.width / 2), window.innerWidth - p.width - 8);
85
+ const above = a.top - p.height - 8;
86
+ setPosition({ left, top: above >= 8 ? above : a.bottom + 8 });
87
+ };
88
+ place();
89
+ window.addEventListener('resize', place);
90
+ return () => { window.removeEventListener('resize', place); };
91
+ }, [anchor, openSections, openMore, othersOpen, snapshot]);
92
+ useEffect(() => {
93
+ const onKey = (event) => { if (event.key === 'Escape')
94
+ onClose(); };
95
+ const onPointer = (event) => {
96
+ const target = event.target;
97
+ if (!(target instanceof Node))
98
+ return;
99
+ if (panel.current?.contains(target) === true || anchor.current?.contains(target) === true)
100
+ return;
101
+ onClose();
102
+ };
103
+ document.addEventListener('keydown', onKey);
104
+ document.addEventListener('pointerdown', onPointer, true);
105
+ return () => {
106
+ document.removeEventListener('keydown', onKey);
107
+ document.removeEventListener('pointerdown', onPointer, true);
108
+ };
109
+ }, [anchor, onClose]);
110
+ const { others } = popoverProviders(shown, snapshot);
111
+ const refreshing = snapshot.refreshing.size > 0;
112
+ const cooling = manualAt !== undefined && now - manualAt < 30_000;
113
+ const toggleIn = (set, id) => {
114
+ const next = new Set(set);
115
+ if (next.has(id))
116
+ next.delete(id);
117
+ else
118
+ next.add(id);
119
+ return next;
120
+ };
121
+ const renderSection = (provider) => {
122
+ const status = snapshot.status?.providers[provider];
123
+ if (status === undefined)
124
+ return null;
125
+ const split = splitAccounts(status);
126
+ if (split === undefined)
127
+ return null;
128
+ const { active, rest } = split;
129
+ const sectionOpen = openSections.has(provider);
130
+ const pinned = prefView.pinned.includes(provider);
131
+ const pinLabel = pinned ? t('unpin') : t('pin');
132
+ return (_jsxs("div", { className: "dsm-psec", children: [_jsxs("div", { className: "dsm-psec-head", children: [_jsxs("button", { type: "button", className: "dsm-fold", "aria-expanded": sectionOpen, onClick: () => { setOpenSections(s => toggleIn(s, provider)); }, children: [_jsxs("span", { className: "dsm-fold-name", children: [_jsx(Icon, { name: "right", size: 12, className: "dsm-chev" }), _jsx("span", { className: "dsm-psec-name", children: shortName(snapshot.status, provider) })] }), sectionOpen ? _jsx("span", {}) : _jsx(Minis, { t: t, windows: windowsOf(snapshot, provider), display: prefView.display, now: now })] }), _jsx("button", { type: "button", className: "dsm-btn dsm-icon dsm-sm dsm-pin", "aria-pressed": pinned, title: pinLabel, "aria-label": pinLabel, onClick: () => { void prefs.togglePinned(provider); }, children: _jsx(Icon, { name: pinned ? 'pinFilled' : 'pin', size: 12 }) })] }), sectionOpen ? (_jsxs("div", { className: "dsm-fold-body", children: [_jsx(Account, { t: t, provider: provider, account: active, status: status, snapshot: snapshot, prefs: prefView, now: now, compact: true }), rest.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "dsm-fold", "aria-expanded": openMore.has(provider), onClick: () => { setOpenMore(s => toggleIn(s, provider)); }, children: [_jsxs("span", { className: "dsm-fold-name", children: [_jsx(Icon, { name: "right", size: 12, className: "dsm-chev" }), _jsx("span", { children: rest.length === 1 ? rest[0].account ?? rest[0].key : tf(t, 'moreAccounts', { count: rest.length }) })] }), rest.length === 1 ? _jsx(MinisFor, { t: t, snapshot: snapshot, provider: provider, account: rest[0].key, prefs: prefView, now: now }) : _jsx("span", {})] }), openMore.has(provider) ? (_jsx("div", { className: "dsm-fold-body", children: rest.map(account => _jsx(Account, { t: t, provider: provider, account: account, status: status, snapshot: snapshot, prefs: prefView, now: now, compact: true }, account.key)) })) : null] }))] })) : null] }, provider));
133
+ };
134
+ return (_jsxs("div", { ref: panel, className: "dsm-pop", role: "dialog", "aria-label": t('usageLabel'), style: position === undefined ? { visibility: 'hidden', left: 0, top: 0 } : { left: position.left, top: position.top }, children: [_jsxs("div", { className: "dsm-pop-head", children: [_jsx("b", { children: t('usage') }), _jsx("button", { type: "button", className: `dsm-btn dsm-icon${refreshing ? ' dsm-spinning' : ''}`, disabled: refreshing || cooling, title: cooling ? tf(t, 'refreshedWait', { seconds: Math.ceil((30_000 - (now - (manualAt ?? now))) / 1000) }) : t('refreshNow'), "aria-label": t('refreshNow'), onClick: () => { setManualAt(Date.now()); void store.refreshUsage('manual'); }, children: _jsx(Icon, { name: cooling && !refreshing ? 'check' : 'refresh' }) }), onOpenSettings === undefined ? null : (_jsx("button", { type: "button", className: "dsm-btn dsm-icon", title: t('usageSettings'), "aria-label": t('usageSettings'), onClick: onOpenSettings, children: _jsx(Icon, { name: "gear" }) }))] }), shown.map(renderSection), shown.length === 0 ? others.map(renderSection) : others.length === 0 ? null : (_jsxs("div", { className: "dsm-psec dsm-others", children: [_jsxs("button", { type: "button", className: "dsm-fold", "aria-expanded": othersOpen, onClick: () => { setOthersOpen(value => !value); }, children: [_jsxs("span", { className: "dsm-fold-name", children: [_jsx(Icon, { name: "right", size: 12, className: "dsm-chev" }), _jsx("span", { className: "dsm-psec-name", children: tf(t, 'otherProviders', { count: others.length }) })] }), _jsx("span", {})] }), othersOpen ? others.map(renderSection) : null] }))] }));
135
+ }
136
+ function MinisFor({ t, snapshot, provider, account, prefs, now }) {
137
+ const entry = snapshot.usage.get(usageKey(provider, account));
138
+ return _jsx(Minis, { t: t, windows: entry?.kind === 'ready' ? entry.usage.windows : undefined, display: prefs.display, now: now });
139
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Mirror of ImageAttachmentRef (packages/attachment/attachment/src/types.ts);
3
+ * the brand on attachmentId is compile-time only, so string suffices here.
4
+ */
5
+ export interface ImageAttachmentRef {
6
+ attachmentId: string;
7
+ mediaType: string;
8
+ bytes: number;
9
+ width: number;
10
+ height: number;
11
+ name?: string;
12
+ }
13
+ /** Loads a session-authorized durable image URL (resolves to a data/blob URL). */
14
+ export type ImageLoader = (attachment: ImageAttachmentRef) => Promise<string>;
15
+ /** Lightbox strings forwarded to the opened preview. */
16
+ export interface ImageLightboxLabels {
17
+ dialog: string;
18
+ close: string;
19
+ }
20
+ /** Message-image strings the owner resolves from its own locale namespace. */
21
+ export interface MessageImageLabels {
22
+ /** Fallback display name for an unnamed image. */
23
+ image: string;
24
+ /** Thumbnail tooltip inviting the original-image preview. */
25
+ open: string;
26
+ /** Accessible thumbnail label; receives the image's display name. */
27
+ openNamed: (label: string) => string;
28
+ /** Loading placeholder shown until bytes resolve. */
29
+ loading: string;
30
+ /** Retry-control label shown when the load fails. */
31
+ loadFailed: string;
32
+ /** Lightbox strings forwarded to the opened preview. */
33
+ lightbox: ImageLightboxLabels;
34
+ }
35
+ /**
36
+ * Compact history renderer with retryable loading and click-to-open original
37
+ * preview. A lone image renders at its `singleFit` size; an image among
38
+ * several renders as a fixed 64px square tile.
39
+ */
40
+ export declare function MessageImage({ attachment, load, variant, labels }: {
41
+ attachment: ImageAttachmentRef;
42
+ load: ImageLoader;
43
+ variant: 'single' | 'tile';
44
+ labels: MessageImageLabels;
45
+ }): import("react").JSX.Element;
46
+ /** Wrapping image group: a lone image renders large, several render as 64px
47
+ * square tiles (same rule as the platform gallery). */
48
+ export declare function ImageGallery({ images, load, labels }: {
49
+ images: readonly {
50
+ attachment: ImageAttachmentRef;
51
+ }[];
52
+ load: ImageLoader;
53
+ labels: MessageImageLabels;
54
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Plugin-local image gallery for generated-image toolviews.
4
+ *
5
+ * Since web-app rc.8, `@deepseek-ai/dsh-client-ui-attachment`'s browser module
6
+ * exports only its cordis plugin surface (`apply`/`inject`) — the React
7
+ * components are no longer package values, so importing `ImageGallery` from it
8
+ * yields `undefined` at runtime and crashes the toolview inside the slot error
9
+ * boundary (the whole call row disappears). This module re-implements the
10
+ * gallery contract the toolview needs (loader-driven thumbnails, retry on
11
+ * failure, click-to-open lightbox) with the same sizing rules as the platform
12
+ * component, keeping this plugin independent of harness component exports.
13
+ */
14
+ import { useCallback, useEffect, useMemo, useState } from 'react';
15
+ /** Display box for a lone image (platform rule): long edge 240px with the
16
+ * rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
17
+ * `object-fit: cover` — and never upscaled past the image's natural size. The
18
+ * crop anchor keeps the top of very tall images and the left of very wide
19
+ * ones, where the informative content usually starts. */
20
+ function singleFit(attachment) {
21
+ const natural = attachment.width / attachment.height;
22
+ const ratio = Math.min(4, Math.max(0.25, natural));
23
+ const box = ratio >= 1 ? { width: 240, height: 240 / ratio } : { width: 240 * ratio, height: 240 };
24
+ const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height);
25
+ return {
26
+ width: Math.max(1, Math.round(box.width * scale)),
27
+ height: Math.max(1, Math.round(box.height * scale)),
28
+ objectPosition: natural < 0.25 ? 'center top' : natural > 4 ? 'left center' : 'center',
29
+ };
30
+ }
31
+ const styles = {
32
+ gallery: { display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-start' },
33
+ frame: {
34
+ display: 'grid', placeItems: 'center', overflow: 'hidden', padding: 0,
35
+ border: '1px solid var(--dsw-alias-border-l2-darkmode-thin)', borderRadius: 8,
36
+ background: 'var(--dsw-alias-interactive-bg-hover-solid)', cursor: 'zoom-in',
37
+ },
38
+ tile: { width: 64, height: 64 },
39
+ img: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },
40
+ loading: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', padding: '0 8px' },
41
+ error: {
42
+ fontSize: 12, color: 'var(--dsw-alias-state-error-primary)', cursor: 'pointer',
43
+ border: '1px solid var(--dsw-alias-border-l2-darkmode-thin)', borderRadius: 8,
44
+ background: 'transparent', padding: '6px 10px',
45
+ },
46
+ overlay: {
47
+ position: 'fixed', inset: 0, zIndex: 1000, display: 'grid', placeItems: 'center',
48
+ background: 'rgba(0, 0, 0, 0.72)', padding: 24,
49
+ },
50
+ overlayImg: { maxWidth: '92vw', maxHeight: '92vh', objectFit: 'contain', borderRadius: 4 },
51
+ close: {
52
+ position: 'absolute', top: 12, right: 12, width: 32, height: 32, display: 'grid',
53
+ placeItems: 'center', border: 'none', borderRadius: '50%', cursor: 'pointer',
54
+ background: 'rgba(255, 255, 255, 0.16)', color: '#fff', fontSize: 16, lineHeight: 1,
55
+ },
56
+ };
57
+ /**
58
+ * Full-viewport original-image preview: backdrop or close-control click and
59
+ * Escape all dismiss; the image itself is inert so a click on it does not
60
+ * fall through to the backdrop dismissal.
61
+ */
62
+ function ImageLightbox({ src, alt, labels, onClose }) {
63
+ useEffect(() => {
64
+ const onKey = (event) => { if (event.key === 'Escape')
65
+ onClose(); };
66
+ window.addEventListener('keydown', onKey);
67
+ return () => { window.removeEventListener('keydown', onKey); };
68
+ }, [onClose]);
69
+ return (_jsxs("div", { role: "dialog", "aria-label": labels.dialog, style: styles.overlay, onClick: onClose, children: [_jsx("img", { src: src, alt: alt, style: styles.overlayImg, onClick: event => { event.stopPropagation(); } }), _jsx("button", { type: "button", "aria-label": labels.close, style: styles.close, onClick: onClose, children: "\u00D7" })] }));
70
+ }
71
+ /**
72
+ * Compact history renderer with retryable loading and click-to-open original
73
+ * preview. A lone image renders at its `singleFit` size; an image among
74
+ * several renders as a fixed 64px square tile.
75
+ */
76
+ export function MessageImage({ attachment, load, variant, labels }) {
77
+ const [src, setSrc] = useState(null);
78
+ const [error, setError] = useState(false);
79
+ const [open, setOpen] = useState(false);
80
+ // Retry re-arms the one load effect below, so every attempt — first load or
81
+ // retry — runs under the same liveness guard and the same reset.
82
+ const [attempt, setAttempt] = useState(0);
83
+ const retry = useCallback(() => { setAttempt(a => a + 1); }, []);
84
+ const close = useCallback(() => { setOpen(false); }, []);
85
+ const fit = useMemo(() => (variant === 'single' ? singleFit(attachment) : undefined), [attachment, variant]);
86
+ useEffect(() => {
87
+ let live = true;
88
+ setError(false);
89
+ setSrc(null);
90
+ void load(attachment).then((url) => { if (live)
91
+ setSrc(url); }).catch(() => { if (live)
92
+ setError(true); });
93
+ return () => { live = false; };
94
+ }, [attachment, load, attempt]);
95
+ const label = attachment.name ?? labels.image;
96
+ if (error) {
97
+ return _jsx("button", { type: "button", style: styles.error, onClick: retry, children: labels.loadFailed });
98
+ }
99
+ const box = fit === undefined ? styles.tile : { width: fit.width, height: fit.height };
100
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: { ...styles.frame, ...box }, title: labels.open, "aria-label": labels.openNamed(label), onClick: () => { if (src !== null)
101
+ setOpen(true); }, children: src === null
102
+ ? _jsx("span", { style: styles.loading, children: labels.loading })
103
+ : _jsx("img", { src: src, alt: label, style: { ...styles.img, objectPosition: fit?.objectPosition } }) }), open && src !== null && _jsx(ImageLightbox, { src: src, alt: label, labels: labels.lightbox, onClose: close })] }));
104
+ }
105
+ /** Wrapping image group: a lone image renders large, several render as 64px
106
+ * square tiles (same rule as the platform gallery). */
107
+ export function ImageGallery({ images, load, labels }) {
108
+ if (images.length === 0)
109
+ return null;
110
+ const variant = images.length === 1 ? 'single' : 'tile';
111
+ return (_jsx("div", { style: styles.gallery, children: images.map((image, index) => (_jsx(MessageImage, { attachment: image.attachment, load: load, variant: variant, labels: labels }, `${image.attachment.attachmentId}:${index}`))) }));
112
+ }
@@ -0,0 +1,48 @@
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 { ImageLoader } from './ImageGallery.js';
4
+ import type { SubscriptionsKey } from './locales.js';
5
+ /** Mirror of ui-tool's ToolCallOwnerProps (see the module header). */
6
+ interface ToolCallOwnerProps {
7
+ callId: string;
8
+ toolName: string;
9
+ block: ToolCallBlock;
10
+ cwd?: string | undefined;
11
+ openFile: (path: string) => void;
12
+ inspect?: (() => void) | undefined;
13
+ }
14
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
15
+ interface SlotMap {
16
+ /** Mirror of ui-tool's keyed atomic Tool view declaration (see the module header). */
17
+ 'tool.call.toolview': {
18
+ kind: 'keyed';
19
+ scope: 'session';
20
+ owner: ToolCallOwnerProps;
21
+ };
22
+ }
23
+ }
24
+ /** Injected dependencies of {@link ImageGenerateToolview} (slot `inject`). */
25
+ export interface ImageGenerateToolviewInjected {
26
+ /** Session-authorized image URL loader riding the `/subscriptions-auth` channel. */
27
+ load: ImageLoader;
28
+ }
29
+ /**
30
+ * Props delivered by the toolview outlet: the owner share plus the inject
31
+ * face and the framework locale seat, spread flat.
32
+ */
33
+ export type ImageGenerateToolviewProps = Partial<ToolCallOwnerProps> & Partial<ImageGenerateToolviewInjected> & {
34
+ t?: ((key: SubscriptionsKey, params?: Record<string, unknown>) => string) | undefined;
35
+ };
36
+ /**
37
+ * Build the ImageGallery loader over the `image` endpoint.
38
+ * @param rpc - Connection RPC caller.
39
+ * @returns loader resolving an attachment ref to a data URL.
40
+ */
41
+ export declare function createImageLoader(rpc: ConnectionHandle['rpc']): ImageLoader;
42
+ /**
43
+ * The `image_generate` keyed toolview component.
44
+ * @param props - owner share, inject face, and locale seat (spread flat).
45
+ * @returns the call row plus, once settled, the gallery / text / error body.
46
+ */
47
+ export declare function ImageGenerateToolview(props: ImageGenerateToolviewProps): import("react").JSX.Element | null;
48
+ export {};
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ImageGallery } from './ImageGallery.js';
3
+ import { Icon } from './meter.js';
4
+ import { call } from './rpc.js';
5
+ import { en } from './locales.js';
6
+ /** Title prompt truncation budget (characters). */
7
+ const PROMPT_MAX_LENGTH = 60;
8
+ /**
9
+ * Build the ImageGallery loader over the `image` endpoint.
10
+ * @param rpc - Connection RPC caller.
11
+ * @returns loader resolving an attachment ref to a data URL.
12
+ */
13
+ export function createImageLoader(rpc) {
14
+ // The host validates a full ImageAttachmentRef payload (readImage takes the
15
+ // whole ref), so forward the attachment verbatim.
16
+ return attachment => call(rpc, 'image', { ...attachment })
17
+ .then(result => `data:${result.mediaType};base64,${result.dataBase64}`);
18
+ }
19
+ /**
20
+ * English-dictionary fallback for a missing locale seat (standalone renders);
21
+ * the framework always supplies the namespace-bound one.
22
+ * @param key - dictionary key.
23
+ * @param params - `{name}` template params.
24
+ * @returns the template with params substituted.
25
+ */
26
+ function fallbackTranslate(key, params) {
27
+ let text = en[key];
28
+ for (const [name, value] of Object.entries(params ?? {})) {
29
+ text = text.replaceAll(`{${name}}`, String(value));
30
+ }
31
+ return text;
32
+ }
33
+ /** Extract the prompt from the call's raw args JSON; falls back to the first string value, then the raw line. */
34
+ function derivePrompt(argsRaw) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(argsRaw);
38
+ }
39
+ catch {
40
+ // Non-JSON args (mid-stream truncation): fall back to the raw string below.
41
+ parsed = undefined;
42
+ }
43
+ let prompt;
44
+ if (typeof parsed === 'object' && parsed !== null) {
45
+ const args = parsed;
46
+ if (typeof args.prompt === 'string' && args.prompt !== '')
47
+ prompt = args.prompt;
48
+ else {
49
+ for (const value of Object.values(args)) {
50
+ if (typeof value === 'string' && value !== '') {
51
+ prompt = value;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ const line = (prompt ?? argsRaw).split('\n', 1)[0] ?? '';
58
+ return line.length > PROMPT_MAX_LENGTH ? `${line.slice(0, PROMPT_MAX_LENGTH)}…` : line;
59
+ }
60
+ /** Flatten a settled result's text blocks (the degraded text-only route and the error line). */
61
+ function resultText(block) {
62
+ if (!('kind' in block))
63
+ return '';
64
+ const parts = [];
65
+ for (const part of block.content) {
66
+ if (part.type === 'text')
67
+ parts.push(part.text);
68
+ }
69
+ if (parts.length === 0 && block.error !== undefined)
70
+ parts.push(`${block.error.name}: ${block.error.code}`);
71
+ return parts.join('\n');
72
+ }
73
+ /** Image attachments of a settled result; empty while running or on the text-only route. */
74
+ function resultImages(block) {
75
+ if (!('kind' in block))
76
+ return [];
77
+ const images = [];
78
+ for (const part of block.content) {
79
+ if (part.type === 'image')
80
+ images.push({ attachment: part.attachment });
81
+ }
82
+ return images;
83
+ }
84
+ const styles = {
85
+ container: { display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0' },
86
+ row: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 },
87
+ icon: { display: 'inline-flex', flexShrink: 0, color: 'var(--dsw-alias-label-tertiary)' },
88
+ title: {
89
+ fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)',
90
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
91
+ },
92
+ subtle: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
93
+ output: {
94
+ margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)',
95
+ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
96
+ },
97
+ error: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
98
+ };
99
+ /**
100
+ * The `image_generate` keyed toolview component.
101
+ * @param props - owner share, inject face, and locale seat (spread flat).
102
+ * @returns the call row plus, once settled, the gallery / text / error body.
103
+ */
104
+ export function ImageGenerateToolview(props) {
105
+ const { block, load } = props;
106
+ const t = props.t ?? fallbackTranslate;
107
+ if (block === undefined)
108
+ return null;
109
+ const settled = 'kind' in block;
110
+ const argsRaw = (settled ? block.call?.argsRaw : 'argsRaw' in block ? block.argsRaw : undefined) ?? '';
111
+ let references = 0;
112
+ try {
113
+ const args = JSON.parse(argsRaw);
114
+ if (Array.isArray(args?.referenceImages))
115
+ references = args.referenceImages.length;
116
+ }
117
+ catch { /* Arguments may still be streaming. */ }
118
+ const title = `image_generate${references > 0 ? ` (${references} ref)` : ''}: ${derivePrompt(argsRaw)}`;
119
+ const images = resultImages(block);
120
+ const text = settled ? resultText(block) : '';
121
+ const labels = {
122
+ image: t('image'),
123
+ open: t('viewImage'),
124
+ openNamed: name => t('viewImageNamed', { name }),
125
+ loading: t('imageLoading'),
126
+ loadFailed: t('imageLoadFailed'),
127
+ lightbox: { dialog: t('imagePreview'), close: t('imageClose') },
128
+ };
129
+ 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('generating') }), settled && block.isError && text !== '' && (_jsx("p", { style: styles.error, children: text.split('\n', 1)[0] })), settled && !block.isError && images.length > 0 && load !== undefined && (_jsx(ImageGallery, { images: images, load: load, labels: labels })), settled && !block.isError && images.length === 0 && text !== '' && (_jsx("p", { style: styles.output, children: text }))] }));
130
+ }
@@ -0,0 +1,15 @@
1
+ import type { StoreSnapshot } from './store.js';
2
+ import type { T } from './meter.js';
3
+ import type { SurfaceInjected } from './context.js';
4
+ export interface PaceNotice {
5
+ id: string;
6
+ tone: 'warn' | 'error';
7
+ text: string;
8
+ }
9
+ /**
10
+ * The most urgent notice across the serving accounts, or undefined.
11
+ * Only serving or default accounts warn; an idle account's pace is not the user's problem yet.
12
+ */
13
+ export declare function currentNotice(t: T, snapshot: StoreSnapshot, now: number, dismissed: ReadonlySet<string>): PaceNotice | undefined;
14
+ export type NoticeProps = Partial<SurfaceInjected>;
15
+ export declare function Notice(props: NoticeProps): import("react").JSX.Element | null;