@linxin666/dsh-usage 0.3.22 → 0.3.24

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 (40) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +6 -8
  3. package/README.zh.md +6 -8
  4. package/lib/client.js +749 -78
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +171 -201
  7. package/lib/types/client/UsageSectionCard.d.ts +0 -1
  8. package/lib/types/client/UsageSidebarPanel.d.ts +34 -0
  9. package/lib/types/client/body-mutations.d.ts +45 -0
  10. package/lib/types/client/locales.d.ts +9 -4
  11. package/lib/types/client/sidebar-entry-core.d.ts +87 -0
  12. package/lib/types/client/sidebar-entry.d.ts +35 -0
  13. package/lib/types/client/sidebar-panel-mount.d.ts +40 -0
  14. package/lib/types/client/voucher.d.ts +2 -2
  15. package/lib/types/core/provider-routes.d.ts +26 -0
  16. package/lib/types/core/types.d.ts +14 -1
  17. package/lib/types/host/access.d.ts +17 -0
  18. package/lib/types/host/pair-access.d.ts +34 -0
  19. package/lib/types/host/routes.d.ts +16 -7
  20. package/lib/types/host/usage-service.d.ts +28 -73
  21. package/lib/types/index.d.ts +0 -2
  22. package/package.json +4 -3
  23. package/src/client/UsageSectionCard.tsx +1 -15
  24. package/src/client/UsageSidebarPanel.tsx +196 -0
  25. package/src/client/body-mutations.ts +133 -0
  26. package/src/client/index.ts +22 -1
  27. package/src/client/locales.ts +20 -10
  28. package/src/client/sidebar-entry-core.ts +321 -0
  29. package/src/client/sidebar-entry.ts +101 -0
  30. package/src/client/sidebar-panel-mount.tsx +113 -0
  31. package/src/client/usage.module.css +252 -0
  32. package/src/client/voucher.ts +2 -2
  33. package/src/core/adapters.ts +40 -6
  34. package/src/core/provider-routes.ts +37 -0
  35. package/src/core/types.ts +14 -1
  36. package/src/host/access.ts +21 -0
  37. package/src/host/pair-access.ts +49 -0
  38. package/src/host/routes.ts +19 -10
  39. package/src/host/usage-service.ts +65 -214
  40. package/src/index.ts +1 -6
@@ -0,0 +1,87 @@
1
+ /**
2
+ * One trailing action button seated at the entry row's right edge (e.g. a
3
+ * refresh command or a collapse chevron). Rows with actions switch from the
4
+ * classic single-button structure to a container with a main button plus the
5
+ * action buttons (nested interactive elements are invalid HTML); rows without
6
+ * actions keep the classic structure untouched.
7
+ */
8
+ export interface SidebarEntryAction {
9
+ /** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
10
+ id: string;
11
+ /** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
12
+ icon: string;
13
+ /**
14
+ * Optional icon while the row is inactive. Setting it makes the action a
15
+ * state mirror (a collapse chevron): the core swaps the icon and stamps
16
+ * aria-expanded on every active-state change. Requires the active bridge.
17
+ */
18
+ inactiveIcon?: string;
19
+ /** Localized accessible label (aria-label + title); receives the active state. */
20
+ label(active: boolean): string;
21
+ /**
22
+ * Click handler; receives the button so the caller can drive transient
23
+ * affordances (a refresh cooldown) without a re-render.
24
+ */
25
+ onClick(button: HTMLButtonElement): void;
26
+ }
27
+ /** Per-package configuration for one sidebar entry row. */
28
+ export interface SidebarEntryOptions {
29
+ /** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
30
+ rowAttribute: string;
31
+ /** CSS selector matching the injected row, e.g. '[data-dsh-ssh-entry]'. */
32
+ rowSelector: string;
33
+ /**
34
+ * L2 semantic-attribute plugin id (issue #506, enum table:
35
+ * skins/skin-center/contracts/semantic-attrs-v1.md). When set, the row also
36
+ * outputs data-dsh-plugin="<id>" and data-dsh-part="sidebar-entry"; unset
37
+ * leaves the row without semantic attributes.
38
+ */
39
+ plugin?: string;
40
+ /** Inline icon markup (matches the shell's 16px nav-icon look). */
41
+ icon: string;
42
+ /** CSS module class names for the row and its two spans (entry / entryIcon / entryLabel). */
43
+ css: Record<string, string>;
44
+ /** Localized row label (aria-label + visible text). */
45
+ label(): string;
46
+ /** Optional localized tooltip (title attribute). */
47
+ tooltip?(): string;
48
+ /**
49
+ * Optional locale-change subscription: re-applies label / aria-label /
50
+ * tooltip whenever the active locale changes. Plain-DOM rows would
51
+ * otherwise keep the label captured at mount; pass the SDK locale runtime
52
+ * subscription (ctx.locale.subscribe) so the row follows the language
53
+ * switch without a reload.
54
+ */
55
+ refresh?: {
56
+ subscribe(listener: () => void): () => void;
57
+ };
58
+ /** Click action (open/toggle the owning panel). */
59
+ onToggle(): void;
60
+ /**
61
+ * Optional trailing action buttons at the row's right edge. Requires the
62
+ * package CSS to seat entryMain / entryAction; empty keeps the classic
63
+ * single-button row.
64
+ */
65
+ actions?: readonly SidebarEntryAction[];
66
+ /** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
67
+ position: 'before' | 'after';
68
+ /**
69
+ * Selectors of the sibling plugin entry rows this package orders against
70
+ * (its own row included — the placement guard excludes a row that is
71
+ * already inside the root). Each package passes the same list it used
72
+ * before the consolidation so the rendered order stays stable.
73
+ */
74
+ familySelectors: readonly string[];
75
+ /** Optional active-state bridge; highlights the row while the panel is open. */
76
+ active?: {
77
+ subscribe(listener: () => void): () => void;
78
+ isOpen(): boolean;
79
+ };
80
+ }
81
+ /**
82
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
83
+ * on later React re-renders.
84
+ * @param options - the row's attribute/icon/copy/action/ordering configuration.
85
+ * @returns disposer removing the entry and its observers.
86
+ */
87
+ export declare function mountSidebarEntry(options: SidebarEntryOptions): () => void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Sidebar entry injection for the usage panel (issue #1592) — wiring over the
3
+ * shared core. Unlike the panel-takeover family (task board / SSH / skill
4
+ * center) this row does not participate in the family block, so it is placed
5
+ * directly under the New Session row and ordered after those entries. The row
6
+ * seats the panel's controls as trailing actions: a refresh command and a
7
+ * collapse chevron mirroring the open state, so the panel below renders only
8
+ * its content.
9
+ * @module @linxin666/dsh-usage/client/sidebar-entry
10
+ */
11
+ /** Stable data attribute identifying the injected entry row. */
12
+ export declare const ENTRY_SELECTOR = "[data-dsh-usage-entry]";
13
+ /** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
14
+ export interface LocaleRefreshSource {
15
+ subscribe(listener: () => void): () => void;
16
+ }
17
+ /** Panel-facing callbacks and state the entry row's controls drive. */
18
+ export interface UsageSidebarEntryHandlers {
19
+ /** Toggles the panel (the row's main area and the collapse chevron). */
20
+ onToggle(): void;
21
+ /** Forces one host probe cycle (the refresh action). */
22
+ onRefresh(): void;
23
+ /** Reads the panel's expanded state (row highlight and chevron direction). */
24
+ isOpen(): boolean;
25
+ /** Subscribes to expanded-state changes (the chevron mirrors them live). */
26
+ subscribeOpen(listener: () => void): () => void;
27
+ }
28
+ /**
29
+ * Mount the usage sidebar entry.
30
+ * @param handlers - the panel's toggle/refresh callbacks and open-state face.
31
+ * @param locale - locale-change source; when given, re-applies the label on a
32
+ * language switch (the plain-DOM row otherwise keeps the mount-time copy).
33
+ * @returns disposer removing the entry and its observers.
34
+ */
35
+ export declare function mountSidebarEntry(handlers: UsageSidebarEntryHandlers, locale?: LocaleRefreshSource): () => void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Sidebar usage panel mounting (issue #1592).
3
+ *
4
+ * The panel is a plain DOM row inserted into the sidebar right below the
5
+ * usage entry row, carrying its own React root. Placement self-heals with the
6
+ * same page-wide body-mutation hub the entry row uses; the entry row stays the
7
+ * anchor, so the sidebar order never depends on this module's timing.
8
+ *
9
+ * The mount owns the panel's single open state: initialized from localStorage,
10
+ * flipped by the entry row's toggle controls, and mirrored to the React body
11
+ * through the subscribe/isOpen face (the same face the entry row's chevron
12
+ * and highlight read).
13
+ * @module @linxin666/dsh-usage/client/sidebar-panel-mount
14
+ */
15
+ import type { UsageStoreInstance } from './usage-store.ts';
16
+ /** localStorage key holding the collapsed flag ('1' = collapsed). */
17
+ export declare const COLLAPSED_STORAGE_KEY = "dsh-usage.sidebar.collapsed";
18
+ /** Panel inputs the mount forwards into the React tree. */
19
+ export interface UsagePanelMountProps {
20
+ store: UsageStoreInstance;
21
+ /** Fetch one overview now. */
22
+ poll: () => void;
23
+ }
24
+ /** Mounted panel controller: the entry row drives the toggle and mirrors the state. */
25
+ export interface UsagePanelMount {
26
+ /** Whether the panel body is currently expanded. */
27
+ isOpen(): boolean;
28
+ /** Subscribe to open/collapse changes (the entry row's chevron and highlight). */
29
+ subscribe(listener: () => void): () => void;
30
+ /** Flip the open state, persist it, and notify subscribers. */
31
+ toggle(): void;
32
+ /** Unmount the panel and remove its container. */
33
+ dispose(): void;
34
+ }
35
+ /**
36
+ * Mount the sidebar usage panel below its entry row.
37
+ * @param props - the store and poll callback of the apply body.
38
+ * @returns the panel controller.
39
+ */
40
+ export declare function mountUsagePanel(props: UsagePanelMountProps): UsagePanelMount;
@@ -21,8 +21,8 @@ export interface VoucherData {
21
21
  from: string;
22
22
  to: string;
23
23
  }
24
- /** Anti-inflation exchange rate: 1000 tokens mint one whale yuan. */
25
- export declare const TOKENS_PER_WHALE_YUAN = 1000;
24
+ /** Anti-inflation exchange rate: 1,000,000 tokens mint one whale yuan. */
25
+ export declare const TOKENS_PER_WHALE_YUAN = 1000000;
26
26
  /** The note's face value in whale yuan; the smallest denomination is 1. */
27
27
  export declare function faceValue(tokens: number): number;
28
28
  /** Local calendar day (`YYYY-MM-DD`) for an epoch ms timestamp. */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Alias folding for the enumerated provider routes. Kept in core because the
3
+ * rule is pure — the host enumerates routes and this decides which of two
4
+ * routes serving one adapter family survives.
5
+ * @module @linxin666/dsh-usage/core/provider-routes
6
+ */
7
+ /** The facts alias folding needs about one enumerated provider route. */
8
+ export interface FoldableRoute {
9
+ /** Provider route key (`deepseek`, `deepseek-official`, custom routes, ...). */
10
+ id: string;
11
+ /** Whether the LLM runtime serves requests on this route; a catalog-only entry is dormant. */
12
+ live: boolean;
13
+ }
14
+ /**
15
+ * Drop dormant catalog aliases that shadow a live route of the same adapter
16
+ * family. The pi-ai catalog ships route keys the runtime serves nothing on
17
+ * until a profile declares them: for DeepSeek the catalog entry `deepseek`
18
+ * sits beside the llm-deepseek runtime route `deepseek-official`, and both
19
+ * resolve the same credential (the family's `apiKeyEnv` fallback), so probing
20
+ * and rendering both turns one account into two identical balance rows.
21
+ *
22
+ * A family keeps every live route it has — two configured profiles are two
23
+ * real accounts, not aliases. A route with no adapter, or one whose family
24
+ * has no live route at all, is returned untouched.
25
+ */
26
+ export declare function foldAliasRoutes<T extends FoldableRoute>(routes: readonly T[]): T[];
@@ -118,12 +118,25 @@ export interface ProviderSnapshotView {
118
118
  export interface UsageOverviewView {
119
119
  updatedAt: number;
120
120
  providers: ProviderSnapshotView[];
121
- /** The provider the pet bubble and the header highlight; `source` says how it was picked. */
121
+ /** The provider the collapsed sidebar strip and the header highlight; `source` says how it was picked. */
122
122
  current: {
123
123
  provider?: string;
124
124
  model?: string;
125
125
  /** `live` — last request seen this boot; `default` — the agent default model. */
126
126
  source: 'live' | 'default';
127
+ /**
128
+ * Resolved display name (snapshot first, route-derived otherwise), so the
129
+ * strip needs no name-resolution of its own. Optional so an older host
130
+ * document still renders.
131
+ */
132
+ displayName?: string;
133
+ /**
134
+ * Today's ledger totals for the provider's adapter family (the route
135
+ * itself when adapter-less), carrying tokens, calls, and the priced cost.
136
+ * Absent on a day without usage; optional for the same older-host
137
+ * tolerance as displayName.
138
+ */
139
+ today?: UsageTokenTotals;
127
140
  };
128
141
  usage: {
129
142
  today: {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Usage trust fence (issue #1592): loopback (the desktop) always passes, and a
3
+ * live paired-device cookie is an additional allow path when remote-web-ui is
4
+ * loaded — the same family fence the skill center uses. Without that service
5
+ * the fence stays loopback-only, so an unpaired LAN browser keeps its 403.
6
+ * The decision logic lives in the generated pair-access.ts copy (shared by
7
+ * git-graph / pet / skill-explorer).
8
+ */
9
+ import type { IncomingMessage } from 'node:http';
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ /**
12
+ * Whether this request may read the usage overview or trigger a refresh.
13
+ * @param ctx - host context; may expose remoteWebUiPairing.
14
+ * @param request - the incoming HTTP request.
15
+ * @returns true for loopback, or a live paired-device cookie.
16
+ */
17
+ export declare function isUsageAllowed(ctx: Context, request: IncomingMessage): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pairing trust fence shared by plugins that expose host routes: loopback
3
+ * (the desktop) always passes; a live paired-device cookie is an additional
4
+ * allow path when remote-web-ui is loaded. The consuming plugin never
5
+ * depends on that plugin — without the service the fence stays
6
+ * loopback-only.
7
+ *
8
+ * Per-package wrappers (access.ts) call this with their own name so each
9
+ * plugin keeps a self-describing export; the security decision lives only
10
+ * here.
11
+ */
12
+ import type { IncomingMessage } from 'node:http';
13
+ /** Structural pairing lookup (no package dependency on remote-web-ui). */
14
+ interface PairingAccess {
15
+ isPairedDevice(request: IncomingMessage): boolean;
16
+ }
17
+ /**
18
+ * Structural host-context shape: shared sources carry no @deepseek-ai
19
+ * dependency (the shared package must typecheck standalone), so the fence
20
+ * reads only the two members it needs; cordis Context satisfies this.
21
+ * ctx.get is optional on the test harness; production Context always has it.
22
+ */
23
+ interface LookupCtx {
24
+ get?(name: string, strict?: boolean): unknown;
25
+ remoteWebUiPairing?: PairingAccess;
26
+ }
27
+ /**
28
+ * Whether this request may enter the plugin's host routes.
29
+ * @param ctx - host context; may expose remoteWebUiPairing.
30
+ * @param request - the incoming HTTP request.
31
+ * @returns true for loopback, or a live paired-device cookie.
32
+ */
33
+ export declare function isPairedOrLoopbackAllowed(ctx: LookupCtx, request: IncomingMessage): boolean;
34
+ export {};
@@ -1,14 +1,23 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
1
2
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
2
3
  import type { UsageService } from './usage-service.ts';
3
4
  export declare const USAGE_API_PREFIX = "/api/dsh-usage";
4
5
  /**
5
- * Loopback-fenced overview route: provider balances, plan quotas, and token
6
- * usage totals. Personal account data, so the loopback fence mirrors
7
- * dsh-perf's stats surface; the browser runs on the same machine.
6
+ * Overview route: provider balances, plan quotas, and token usage totals.
7
+ * Personal account data behind the family trust fence — loopback always
8
+ * passes, and a live paired-device cookie passes too when remote-web-ui is
9
+ * loaded (issue #1592: the sidebar usage panel reads this from a paired LAN
10
+ * browser); every unpaired LAN request keeps its 403.
11
+ * @param ctx - host context; may expose remoteWebUiPairing.
12
+ * @param service - the usage service.
13
+ * @returns the route.
8
14
  */
9
- export declare function makeUsageOverviewRoute(service: UsageService): WebRoute;
15
+ export declare function makeUsageOverviewRoute(ctx: Context, service: UsageService): WebRoute;
10
16
  /**
11
- * Loopback-fenced manual refresh: forces one probe cycle now and answers
12
- * with the fresh overview.
17
+ * Manual refresh: forces one probe cycle now and answers with the fresh
18
+ * overview. Same trust fence as the overview route.
19
+ * @param ctx - host context; may expose remoteWebUiPairing.
20
+ * @param service - the usage service.
21
+ * @returns the route.
13
22
  */
14
- export declare function makeUsageRefreshRoute(service: UsageService): WebRoute;
23
+ export declare function makeUsageRefreshRoute(ctx: Context, service: UsageService): WebRoute;
@@ -1,15 +1,13 @@
1
1
  /**
2
2
  * The dsh-usage host service: folds live session usage into the persistent
3
3
  * ledger, probes each configured provider's balance/coding-plan endpoint on
4
- * a poll cycle, and announces the current provider's status to the pet
5
- * bubble. Secrets stay in the host process; the browser only ever sees the
6
- * overview document.
4
+ * a poll cycle, and serves the current provider's strip-ready view inside
5
+ * the overview document. Secrets stay in the host process; the browser only
6
+ * ever sees the overview document.
7
7
  * @module @linxin666/dsh-usage/host/usage-service
8
8
  */
9
9
  import type { Context } from '@deepseek-ai/cordis';
10
- import type { ProviderSnapshotView, UsageOverviewView, UsageTokenTotals } from '../core/types.ts';
11
- /** Source tag the plugin stamps onto pet announcements. */
12
- export declare const USAGE_ANNOUNCE_SOURCE = "dsh-usage";
10
+ import type { UsageOverviewView } from '../core/types.ts';
13
11
  /**
14
12
  * Persisted accrual state for the official DeepSeek family's real spend:
15
13
  * consecutive observations of the official CNY balance, decreases counted
@@ -25,54 +23,11 @@ export interface SpendWatch {
25
23
  /** The balance the previous observation ended on, when one exists. */
26
24
  lastBalanceCny?: number;
27
25
  }
28
- /** Poll-loop and announce options; re-applied live on settings change. */
26
+ /** Poll-loop options; re-applied live on settings change. */
29
27
  export interface UsageServiceOptions {
30
28
  pollIntervalSec: number;
31
- bubbleMode: 'always' | 'change' | 'off';
32
29
  retainDays: number;
33
30
  }
34
- /** Format a balance for display: symbol prefix when known, code suffix otherwise. */
35
- export declare function formatMoney(currency: string, totalBalance: string): string;
36
- /** Map a used percent to the announcement tone. */
37
- export declare function planTone(percent: number): 'ok' | 'warn' | 'low';
38
- /** Announcement context the service computes per poll: today's family spend and the DeepSeek period. */
39
- export interface AnnounceContext {
40
- /** Today's ledger spend for the announced provider's family (CNY; 0 = unpriced). */
41
- todayCost?: number;
42
- /** Whether DeepSeek peak pricing is in effect right now. */
43
- peak?: boolean;
44
- }
45
- /**
46
- * Build the raw pet announce payload for one provider snapshot, or
47
- * undefined when nothing worth announcing exists. Pure: every payload this
48
- * returns satisfies the pet's `parseAnnouncement` contract — plan
49
- * announcements require a numeric percent, so percent-less windows never
50
- * announce (the pet validator would silently drop them). A priced family
51
- * (DeepSeek) with spend today announces a cost bubble first; balance-only
52
- * families announce the balance; plan families announce their tightest
53
- * percent window.
54
- */
55
- export declare function buildAnnouncement(snapshot: Pick<ProviderSnapshotView, 'displayName' | 'balance' | 'plan'>, context?: AnnounceContext): Record<string, unknown> | undefined;
56
- /**
57
- * Compact token-count display for the usage fallback bubble: `9805`,
58
- * `8.7万`, `1.2亿`. The host-authored bubble copy is zh (like every other
59
- * line this service speaks), so the magnitudes follow the zh convention.
60
- */
61
- export declare function formatTokens(count: number): string;
62
- /**
63
- * The usage fallback for the session's provider: today's token consumption
64
- * and call count, the one fact the ledger owns for every provider. Providers
65
- * without a readable balance/plan endpoint (relay stations, local runtimes,
66
- * token-plan vendors — and probeable providers whose probes are failing)
67
- * would otherwise leave the pet bubble permanently silent even while their
68
- * sessions run. Undefined when the provider has no usage today: a bubble
69
- * about nothing is noise, not information. Pure; the payload satisfies the
70
- * pet's `parseAnnouncement` contract.
71
- */
72
- export declare function buildLedgerAnnouncement(input: {
73
- displayName: string;
74
- totals: UsageTokenTotals;
75
- }): Record<string, unknown> | undefined;
76
31
  export declare class UsageService {
77
32
  private readonly ctx;
78
33
  private options;
@@ -85,7 +40,7 @@ export declare class UsageService {
85
40
  private spendWatch;
86
41
  /** Per-live-session route attribution (WeakMap: disposed sessions age out). */
87
42
  private readonly sessionRoutes;
88
- /** The most recent route seen this boot; the pet bubble follows it. */
43
+ /** The most recent route seen this boot; the sidebar strip and the header highlight follow it. */
89
44
  private current;
90
45
  private sessionListenerDisposer;
91
46
  private pollTimer;
@@ -98,7 +53,6 @@ export declare class UsageService {
98
53
  /** True once loadPersisted finished; nothing may overwrite the files before that. */
99
54
  private loaded;
100
55
  private disposed;
101
- private lastSignature;
102
56
  /** Last prune guard: once per local day, and whenever retention shrinks. */
103
57
  private lastPrune;
104
58
  constructor(ctx: Context, options: UsageServiceOptions);
@@ -116,12 +70,22 @@ export declare class UsageService {
116
70
  refresh(): Promise<void>;
117
71
  /** Assemble the overview document the browser section renders. */
118
72
  overview(): UsageOverviewView;
73
+ /**
74
+ * The strip-ready current-provider view: the resolved display name plus
75
+ * today's ledger totals for the provider's adapter family (the route
76
+ * itself when adapter-less). `today` is absent on a day without usage —
77
+ * a strip about nothing is noise, not information.
78
+ */
79
+ private currentView;
119
80
  /** One local day aggregated per provider. */
120
81
  private daySummary;
121
- /** Today's ledger spend for one provider's adapter family (0 when unpriced). */
122
- private familyCostToday;
123
- /** Today's ledger totals for one provider route (the exact id, not the family). */
124
- private providerUsageToday;
82
+ /**
83
+ * Today's ledger totals for one provider's adapter family — the exact
84
+ * route when adapter-less, the merged family otherwise, so aliased routes
85
+ * of one account (the deepseek catalog id and its runtime alias) fold
86
+ * together.
87
+ */
88
+ private familyUsageToday;
125
89
  /**
126
90
  * Accrue the official DeepSeek family's real spend from observed CNY
127
91
  * balance decreases; a balance rise is a top-up and never counts. The
@@ -134,7 +98,7 @@ export declare class UsageService {
134
98
  /**
135
99
  * The display name for a route key: the LLM runtime's first, then the
136
100
  * adapter's, then the id itself — the snapshot may not exist for
137
- * adapter-less routes, yet the bubble still needs a legible title.
101
+ * adapter-less routes, yet the sidebar strip still needs a legible title.
138
102
  */
139
103
  private routeDisplayName;
140
104
  private onSessionEvent;
@@ -162,11 +126,17 @@ export declare class UsageService {
162
126
  private persistSnapshots;
163
127
  private rearmPoll;
164
128
  /**
165
- * One poll cycle: enumerate routes, resolve credentials, probe, announce.
129
+ * One poll cycle: enumerate routes, resolve credentials, probe.
166
130
  * A call while a cycle is already running joins that cycle instead of
167
131
  * returning immediately, so a manual refresh always waits for real probes.
168
132
  */
169
133
  pollNow(): Promise<void>;
134
+ /**
135
+ * Every route the service probes and renders: the runtime's live providers
136
+ * plus the configurable directory's catalog entries, then alias-folded so a
137
+ * dormant catalog entry cannot shadow the live route of the same adapter
138
+ * family (see foldAliasRoutes).
139
+ */
170
140
  private listProviderRoutes;
171
141
  private probeRoute;
172
142
  /**
@@ -182,19 +152,4 @@ export declare class UsageService {
182
152
  private piAiProfile;
183
153
  /** The DeepSeek official adapter's credential reference name. */
184
154
  private deepseekApiKeyEnv;
185
- /**
186
- * Announce the current provider's spend, balance, or plan usage to the pet.
187
- * In `change` mode only meaningful value changes re-announce; `off` skips.
188
- * The TTL rides the poll interval (bubble_mode `always` re-announces every
189
- * cycle, so a TTL of two cycles + margin keeps the bubble continuous
190
- * across polls; the pet contract caps the ceiling). A route id the
191
- * catalogs spell differently than the snapshot keys falls back to its
192
- * adapter family's snapshot. When the provider has no announceable probe
193
- * fact (no adapter, failing probes, percent-less windows) the bubble falls
194
- * back to the provider's today ledger usage, and with neither fact nor
195
- * usage it stays silent. Fully guarded: a malformed snapshot or a failing
196
- * pet service must never break the poll loop, and a disposed service never
197
- * announces.
198
- */
199
- private announceCurrent;
200
155
  }
@@ -9,8 +9,6 @@ export interface Config {
9
9
  enabled?: boolean;
10
10
  /** Provider probe cycle in seconds; 30-3600. */
11
11
  pollIntervalSec?: number;
12
- /** Pet bubble mode: always (refreshes each poll), change (only on value change), off. */
13
- bubbleMode?: string;
14
12
  /** Ledger retention in local days. */
15
13
  retainDays?: number;
16
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-usage",
3
- "description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with a dedicated pet bubble for the current provider",
4
- "version": "0.3.22",
3
+ "description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with the current session provider's today usage on the sidebar entry",
4
+ "version": "0.3.24",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -66,11 +66,12 @@
66
66
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
67
67
  "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
68
68
  "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
69
- "@linxin666/dsh-pet": "0.3.22",
69
+ "@linxin666/dsh-pet": "0.3.24",
70
70
  "@testing-library/dom": "^10.4.1",
71
71
  "@testing-library/react": "^16.3.2",
72
72
  "@types/node": "^22.20.0",
73
73
  "@types/react": "~18.3.1",
74
+ "@types/react-dom": "^18.3.5",
74
75
  "jsdom": "29.1.1",
75
76
  "react": "^18.2.0",
76
77
  "react-dom": "^18.2.0",
@@ -22,7 +22,6 @@ import type { ObservedSpendView, ProviderSnapshotView, UsageOverviewView, UsageP
22
22
  export interface UsageSettings {
23
23
  enabled?: boolean
24
24
  pollIntervalSec?: number
25
- bubbleMode?: string
26
25
  }
27
26
 
28
27
  /** The registration-side face the section's slot entry injects. */
@@ -401,7 +400,7 @@ function totalOf(totals: UsageTokenTotals): number {
401
400
 
402
401
  /**
403
402
  * The Token 银行 card: the DeepSeek official family's retained-ledger usage
404
- * minted onto the whale-yuan note at 1000 tokens per whale yuan. The window
403
+ * minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The window
405
404
  * prefers the host's whole-ledger aggregate and falls back to the 30-day
406
405
  * trend when an older host serves no `all`; the spend line prefers the
407
406
  * official balance watch and falls back to the fold-time estimate; the
@@ -541,7 +540,6 @@ function SettingsRow(props: {
541
540
  }): ReactNode {
542
541
  const { settings, snapshot, value } = props
543
542
  const disabled = snapshot === undefined || !snapshot.writable
544
- const bubbleMode = typeof value.bubbleMode === 'string' && ['always', 'change', 'off'].includes(value.bubbleMode) ? value.bubbleMode : 'always'
545
543
  return (
546
544
  <div className={styles.card} data-dsh-part="settings-row">
547
545
  <span className={styles.cardTitle}>{t('usage.config.title')}</span>
@@ -569,18 +567,6 @@ function SettingsRow(props: {
569
567
  }}
570
568
  />
571
569
  </label>
572
- <label className={styles.settingItem}>
573
- {t('usage.config.bubbleMode')}
574
- <select
575
- value={bubbleMode}
576
- disabled={disabled}
577
- onChange={(event) => { void settings.set('bubbleMode', event.target.value) }}
578
- >
579
- <option value="always">{t('usage.config.bubbleMode.always')}</option>
580
- <option value="change">{t('usage.config.bubbleMode.change')}</option>
581
- <option value="off">{t('usage.config.bubbleMode.off')}</option>
582
- </select>
583
- </label>
584
570
  </div>
585
571
  </div>
586
572
  )