@linxin666/dsh-usage 0.3.23 → 0.4.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 (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +7 -9
  3. package/README.zh.md +7 -9
  4. package/icon.svg +3 -0
  5. package/lib/client.js +621 -65
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +258 -271
  8. package/lib/types/client/UsageFootCard.d.ts +55 -0
  9. package/lib/types/client/UsageSectionCard.d.ts +3 -4
  10. package/lib/types/client/body-mutations.d.ts +45 -0
  11. package/lib/types/client/foot-card-mount.d.ts +34 -0
  12. package/lib/types/client/index.d.ts +21 -8
  13. package/lib/types/client/locales.d.ts +6 -4
  14. package/lib/types/core/provider-routes.d.ts +26 -0
  15. package/lib/types/core/types.d.ts +14 -1
  16. package/lib/types/host/access.d.ts +17 -0
  17. package/lib/types/host/http.d.ts +18 -1
  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 +32 -76
  21. package/lib/types/index.d.ts +14 -2
  22. package/package.json +20 -17
  23. package/src/client/UsageFootCard.tsx +300 -0
  24. package/src/client/UsageSectionCard.tsx +41 -23
  25. package/src/client/body-mutations.ts +133 -0
  26. package/src/client/foot-card-mount.tsx +112 -0
  27. package/src/client/index.ts +57 -16
  28. package/src/client/locales.ts +12 -8
  29. package/src/client/usage.module.css +188 -0
  30. package/src/core/adapters.ts +40 -6
  31. package/src/core/provider-routes.ts +37 -0
  32. package/src/core/types.ts +14 -1
  33. package/src/host/access.ts +21 -0
  34. package/src/host/http.ts +23 -1
  35. package/src/host/pair-access.ts +49 -0
  36. package/src/host/routes.ts +19 -10
  37. package/src/host/usage-service.ts +74 -218
  38. package/src/index.ts +59 -60
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sidebar foot card: the compact usage glance seated below the shell sidebar's
3
+ * Settings row. It is deliberately a GLANCE, not the panel the 2026-09-18
4
+ * removal took out. Two states, persisted across reloads:
5
+ *
6
+ * - expanded (default): a price-first headline (today's estimated spend,
7
+ * falling back to today's tokens when nothing priced was recorded), a
8
+ * tokens/calls line, up to two configured-provider balances, and an
9
+ * updated-at footer;
10
+ * - collapsed: a one-line strip with the gauge glyph, the spending
11
+ * provider's name, the label and the headline value.
12
+ *
13
+ * The card body is one button that opens the settings panel on the usage
14
+ * section, so detail lives in exactly one place; the corner chevron toggles
15
+ * the collapse state (sibling buttons — buttons inside a button are invalid
16
+ * HTML).
17
+ *
18
+ * Data comes from the same shared store the settings section reads; the card
19
+ * runs its own relaxed poll loop (30 s, visible-tab only) because the sidebar
20
+ * foot is permanently mounted. The card disappears while the plugin is
21
+ * disabled (settings flag) or while the host serves no usage routes (a 404
22
+ * means the host half is off), matching the section's own gating.
23
+ * @module @linxin666/dsh-usage/client/UsageFootCard
24
+ */
25
+ import { type ReactNode } from 'react';
26
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
27
+ import { type UsageSettings } from './UsageSectionCard.tsx';
28
+ import type { UsageStoreInstance } from './usage-store.ts';
29
+ /** Props the mount forwards into the React tree. */
30
+ export interface UsageFootCardProps {
31
+ /** The shared overview store (section and card read the same snapshot). */
32
+ store: UsageStoreInstance;
33
+ /** Fetch one overview now. */
34
+ poll: () => void;
35
+ /** Card-body click: open the settings panel on the usage section. */
36
+ onOpen: () => void;
37
+ /** The shared settings form; the card hides while the plugin is disabled. */
38
+ settings: ConfigForm<UsageSettings>;
39
+ /** Locale-change source; the plain copy re-renders on a language switch. */
40
+ locale?: {
41
+ subscribe(listener: () => void): () => void;
42
+ };
43
+ }
44
+ /** Poll cadence of the permanently seated card; the section's 10 s loop stays its own. */
45
+ export declare const FOOT_CARD_POLL_MS = 30000;
46
+ /** localStorage key holding the collapsed flag ('1' = collapsed strip). */
47
+ export declare const FOOT_CARD_COLLAPSED_KEY = "dsh-usage.foot-card.collapsed";
48
+ /** Read the persisted collapsed flag (absent = expanded). */
49
+ export declare function readFootCardCollapsed(): boolean;
50
+ /**
51
+ * Render the foot card content for the current store snapshot.
52
+ * @param props - store, poller, settings form and open callback from the mount.
53
+ * @returns the card, or null while disabled / host-off.
54
+ */
55
+ export declare function UsageFootCard(props: UsageFootCardProps): ReactNode;
@@ -8,13 +8,12 @@
8
8
  * @module @linxin666/dsh-usage/client/UsageSectionCard
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
11
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
12
12
  import type { UsageStoreInstance } from './usage-store.ts';
13
13
  /** The settings fields this section edits (immediate-apply semantics). */
14
14
  export interface UsageSettings {
15
15
  enabled?: boolean;
16
16
  pollIntervalSec?: number;
17
- bubbleMode?: string;
18
17
  }
19
18
  /** The registration-side face the section's slot entry injects. */
20
19
  export interface UsageSectionFace {
@@ -24,8 +23,8 @@ export interface UsageSectionFace {
24
23
  poll: () => void;
25
24
  /** Force a host probe cycle now (resolves with the fresh overview). */
26
25
  refresh: () => void;
27
- /** Whether a forced refresh is in flight (component-local state mirrors it). */
28
- settings: SettingsScope<UsageSettings>;
26
+ /** The shared configuration form this section's settings row reads and writes. */
27
+ settings: ConfigForm<UsageSettings>;
29
28
  }
30
29
  export interface UsageSectionProps extends UsageSectionFace {
31
30
  /** Close the settings panel (the shell owns the open state). */
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One `document.body` MutationObserver per page, shared by every family
3
+ * plugin bundle.
4
+ *
5
+ * Family plugins that inject at the DOM level must notice when the shell
6
+ * re-renders around their injected node: the sidebar entry rows
7
+ * (sidebar-entry-core), the center-column panel containers
8
+ * (panel-mount-core), and the aggregate shell's column shims. Each consumer
9
+ * used to install its OWN `MutationObserver` on `document.body` with
10
+ * `{ childList: true, subtree: true }`, so a page carrying N family plugins
11
+ * paid N native observers and N callback invocations for EVERY mutation batch
12
+ * the app produced — chat token streaming alone produces many per second, and
13
+ * the count grows with the number of installed plugins.
14
+ *
15
+ * This module keeps exactly ONE body observer per page. The registry lives on
16
+ * `globalThis` under a `Symbol.for` key, so the per-package generated copies
17
+ * of this file (scripts/sync-shared.mjs) and separately bundled plugins all
18
+ * reach the same hub at runtime instead of one hub per module instance.
19
+ * Record subscribers receive the mutations accumulated since the last flush.
20
+ * Consumers that only re-check their DOM use subscribeBodyInvalidations:
21
+ * when all subscribers use that path, the hub retains no mutation records
22
+ * (or detached subtrees) while a background page's animation frames pause.
23
+ * Both paths run at most once per frame. The last subscriber disconnects the
24
+ * observer, cancels the pending frame and releases its records.
25
+ *
26
+ * Failure policy: without a DOM or `MutationObserver` the subscription is a
27
+ * no-op disposer (the same silence the per-consumer observers had), and when
28
+ * `requestAnimationFrame` is unavailable the flush runs synchronously so a
29
+ * subscriber is never silently dropped. A throwing subscriber cannot stop the
30
+ * others.
31
+ */
32
+ /**
33
+ * Subscribe to a coalesced DOM re-check without retaining mutation records.
34
+ * The marked wrapper also works with an older hub, which delivers records
35
+ * that it simply ignores until a page reload picks up the updated hub.
36
+ */
37
+ export declare function subscribeBodyInvalidations(subscriber: () => void): () => void;
38
+ /**
39
+ * Subscribe to body-level childList mutations.
40
+ * @param subscriber - called at most once per animation frame with the records
41
+ * collected since the previous flush; must be safe to run repeatedly.
42
+ * @returns the disposer removing this subscriber (and the observer when it was
43
+ * the last one).
44
+ */
45
+ export declare function subscribeBodyMutations(subscriber: (records: MutationRecord[]) => void): () => void;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sidebar foot card mounting.
3
+ *
4
+ * The foot's only slot (sidebar.footer.action) stacks ABOVE the Settings row
5
+ * and cannot host a block, so the card container is inserted into the shell's
6
+ * foot area directly above the Settings seat — keeping Settings the foot's
7
+ * terminal row instead of hanging a card off the sidebar floor. It self-heals
8
+ * with the page-wide body-mutation hub: a React re-render that displaces the
9
+ * container re-seats it on the next frame, and a whole-pane rebuild is noticed
10
+ * by the same body-level watcher the family entry rows use.
11
+ *
12
+ * The container is a plain div carrying its own React root, so it can never
13
+ * disturb the shell's reconciliation.
14
+ * @module @linxin666/dsh-usage/client/foot-card-mount
15
+ */
16
+ import { type UsageFootCardProps } from './UsageFootCard.tsx';
17
+ /** Stable data attribute identifying the injected card container. */
18
+ export declare const FOOT_CARD_SELECTOR = "[data-dsh-usage-foot-card]";
19
+ /**
20
+ * Open the settings panel on the usage section. The shell owns the panel's
21
+ * open state and exposes no service for it, so the card replays the user's
22
+ * own path: activate the sidebar Settings trigger, then pick the nav row
23
+ * carrying the section's (localized) label. Every step degrades silently —
24
+ * with the panel already open only the nav pick runs, and a missing row
25
+ * leaves the panel on its default section.
26
+ * @param label - the usage section's nav label in the active locale.
27
+ */
28
+ export declare function openUsageSettings(label: () => string): void;
29
+ /**
30
+ * Mount the sidebar foot card directly above the Settings row.
31
+ * @param props - the store/poll/settings/open inputs of the apply body.
32
+ * @returns disposer removing the container and its observers.
33
+ */
34
+ export declare function mountUsageFootCard(props: UsageFootCardProps): () => void;
@@ -1,27 +1,40 @@
1
1
  /**
2
2
  * dsh-usage browser half — seats the first-level 使用统计 settings section
3
- * (below the Workshop entry) and polls the host overview only while the
4
- * section is open. All provider probing and credential handling happens in
5
- * the host half; this bundle only renders the overview document.
3
+ * (below the Workshop entry) plus the compact usage glance card below the
4
+ * sidebar's Settings row. The section polls the host overview only while it
5
+ * is open; the foot card runs its own relaxed loop because the sidebar foot
6
+ * is permanently mounted. All provider probing and credential handling
7
+ * happens in the host half; this bundle only renders the overview document.
6
8
  * @module @linxin666/dsh-usage/client
7
9
  */
8
10
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
9
- import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
11
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
10
12
  import { type UsageSettings } from './UsageSectionCard.tsx';
11
13
  /** Required services. */
12
14
  export declare const inject: string[];
13
15
  export type { UsageSectionProps, UsageSectionFace } from './UsageSectionCard.tsx';
14
16
  export type { UsageUiState } from './usage-store.ts';
15
17
  export type { UsageSettings };
18
+ /**
19
+ * One settings namespace a family card binds. The 0.1.7 client exports no spec
20
+ * type (the form controller takes it privately), so the binder's input shape is
21
+ * restated here.
22
+ */
23
+ export interface UsageSettingsBindSpec<T> {
24
+ /** Settings namespace registered by the owning host plugin. */
25
+ namespace: string;
26
+ /** Narrow one wire section; undefined keeps the last accepted value. */
27
+ decode?: (section: unknown) => T | undefined;
28
+ }
16
29
  declare module '@deepseek-ai/cordis' {
17
30
  interface Context {
18
31
  /**
19
- * Optional rc.6 compatibility binder provided by dsh-web-settings;
20
- * absent when that group plugin is not installed, so callers fall back to
21
- * the official settings scope.
32
+ * Optional family settings binder provided by dsh-web-settings; absent when
33
+ * that group plugin is not installed, so callers fall back to the native
34
+ * per-entry forms (`ctx.configForms`).
22
35
  */
23
36
  webUiSettings?: {
24
- bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S>;
37
+ bind<S>(spec: UsageSettingsBindSpec<S>): ConfigForm<S>;
25
38
  };
26
39
  }
27
40
  }
@@ -46,10 +46,7 @@ export declare const zh: {
46
46
  'usage.config.title': string;
47
47
  'usage.config.enabled': string;
48
48
  'usage.config.pollIntervalSec': string;
49
- 'usage.config.bubbleMode': string;
50
- 'usage.config.bubbleMode.always': string;
51
- 'usage.config.bubbleMode.change': string;
52
- 'usage.config.bubbleMode.off': string;
49
+ 'usage.config.saveFailed': string;
53
50
  'usage.bank.title': string;
54
51
  'usage.bank.hint': string;
55
52
  'usage.bank.noUsage': string;
@@ -60,6 +57,11 @@ export declare const zh: {
60
57
  'usage.bank.save': string;
61
58
  'usage.bank.share': string;
62
59
  'usage.bank.drawError': string;
60
+ 'usage.foot.cost': string;
61
+ 'usage.foot.noData': string;
62
+ 'usage.foot.open': string;
63
+ 'usage.foot.collapse': string;
64
+ 'usage.foot.expand': string;
63
65
  };
64
66
  /** English mirror; every zh key present. */
65
67
  export declare const en: Record<UsageKey, string>;
@@ -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;
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Shared JSON body/response helpers for the host route families: one strict
3
3
  * bounded body reader, one lenient bounded body reader, one JSON object
4
- * narrow, and one JSON writer. Previously these were copy-pasted across the
4
+ * narrow, one JSON writer, and the outbound request-init helper that keeps a
5
+ * host fetch body decodable (withIdentityEncoding). Previously the readers
6
+ * were copy-pasted across the
5
7
  * package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
6
8
  * family's route module) with drifting contracts: body caps ranging 4 KiB to
7
9
  * 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
@@ -34,6 +36,21 @@ export declare function readJsonBody(req: IncomingMessage, opts?: {
34
36
  }): Promise<unknown | null>;
35
37
  /** Narrow a value to a JSON object, or undefined when it is not one. */
36
38
  export declare function asJsonObject(value: unknown): Record<string, unknown> | undefined;
39
+ /**
40
+ * Request init that asks a remote origin for an uncompressed body.
41
+ *
42
+ * The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
43
+ * the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
44
+ * Node's built-in `fetch()` reads. That cross-major wrapper drops the
45
+ * `content-encoding` header and automatic decompression, so a plain fetch
46
+ * resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
47
+ * consumer can read. Every host fetch that parses a remote body must request
48
+ * identity encoding. Caller headers survive; `accept-encoding` is forced to
49
+ * `identity` because a caller value has no decoder behind it in this host.
50
+ * @param init - the caller's request init (signal, headers, method, ...).
51
+ * @returns a copy with `accept-encoding: identity` merged in.
52
+ */
53
+ export declare function withIdentityEncoding(init?: RequestInit): RequestInit;
37
54
  /**
38
55
  * Write one JSON response. Default headers are the family defaults
39
56
  * (content-type and referrer-policy); caller headers are appended or
@@ -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);
@@ -106,22 +60,33 @@ export declare class UsageService {
106
60
  start(): void;
107
61
  /**
108
62
  * Stop timers and flush pending ledger writes. The returned promise
109
- * resolves after the final flush lands, so a successor instance (quick
110
- * disable → enable) can serialize its first load behind it.
63
+ * resolves after the final flush lands, so a successor instance (the Host
64
+ * reloads the profile row on a config change) can serialize its first load
65
+ * behind it.
111
66
  */
112
67
  stop(): Promise<void>;
113
- /** Re-apply options live (settings change); retention shrink prunes now. */
68
+ /** Re-apply options to this running instance; retention shrink prunes now. */
114
69
  applyOptions(options: UsageServiceOptions): void;
115
70
  /** Force one poll now (manual refresh route); joins an in-flight cycle. */
116
71
  refresh(): Promise<void>;
117
72
  /** Assemble the overview document the browser section renders. */
118
73
  overview(): UsageOverviewView;
74
+ /**
75
+ * The strip-ready current-provider view: the resolved display name plus
76
+ * today's ledger totals for the provider's adapter family (the route
77
+ * itself when adapter-less). `today` is absent on a day without usage —
78
+ * a strip about nothing is noise, not information.
79
+ */
80
+ private currentView;
119
81
  /** One local day aggregated per provider. */
120
82
  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;
83
+ /**
84
+ * Today's ledger totals for one provider's adapter family — the exact
85
+ * route when adapter-less, the merged family otherwise, so aliased routes
86
+ * of one account (the deepseek catalog id and its runtime alias) fold
87
+ * together.
88
+ */
89
+ private familyUsageToday;
125
90
  /**
126
91
  * Accrue the official DeepSeek family's real spend from observed CNY
127
92
  * balance decreases; a balance rise is a top-up and never counts. The
@@ -134,7 +99,7 @@ export declare class UsageService {
134
99
  /**
135
100
  * The display name for a route key: the LLM runtime's first, then the
136
101
  * adapter's, then the id itself — the snapshot may not exist for
137
- * adapter-less routes, yet the bubble still needs a legible title.
102
+ * adapter-less routes, yet the sidebar strip still needs a legible title.
138
103
  */
139
104
  private routeDisplayName;
140
105
  private onSessionEvent;
@@ -162,11 +127,17 @@ export declare class UsageService {
162
127
  private persistSnapshots;
163
128
  private rearmPoll;
164
129
  /**
165
- * One poll cycle: enumerate routes, resolve credentials, probe, announce.
130
+ * One poll cycle: enumerate routes, resolve credentials, probe.
166
131
  * A call while a cycle is already running joins that cycle instead of
167
132
  * returning immediately, so a manual refresh always waits for real probes.
168
133
  */
169
134
  pollNow(): Promise<void>;
135
+ /**
136
+ * Every route the service probes and renders: the runtime's live providers
137
+ * plus the configurable directory's catalog entries, then alias-folded so a
138
+ * dormant catalog entry cannot shadow the live route of the same adapter
139
+ * family (see foldAliasRoutes).
140
+ */
170
141
  private listProviderRoutes;
171
142
  private probeRoute;
172
143
  /**
@@ -182,19 +153,4 @@ export declare class UsageService {
182
153
  private piAiProfile;
183
154
  /** The DeepSeek official adapter's credential reference name. */
184
155
  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
156
  }
@@ -4,13 +4,17 @@ import z from 'schemastery';
4
4
  import { type UsageServiceOptions } from './host/usage-service.ts';
5
5
  export declare const name = "dsh-usage";
6
6
  export declare const inject: string[];
7
+ /**
8
+ * Family settings namespace this row serves. Since 0.1.7 the settings surface
9
+ * addresses one form per profile entry id and the Host generates this row's
10
+ * page from the Config schema below, so the namespace is only the alias the
11
+ * family settings bridge (dsh-web-settings) resolves onto this row's entry id.
12
+ */
7
13
  export declare const USAGE_SETTINGS_NAMESPACE: SettingsNamespace;
8
14
  export interface Config {
9
15
  enabled?: boolean;
10
16
  /** Provider probe cycle in seconds; 30-3600. */
11
17
  pollIntervalSec?: number;
12
- /** Pet bubble mode: always (refreshes each poll), change (only on value change), off. */
13
- bubbleMode?: string;
14
18
  /** Ledger retention in local days. */
15
19
  retainDays?: number;
16
20
  }
@@ -19,4 +23,12 @@ export interface ResolvedConfig extends UsageServiceOptions {
19
23
  enabled: boolean;
20
24
  }
21
25
  export declare function resolveConfig(config?: Config): ResolvedConfig;
26
+ /**
27
+ * Host body. The plugin's own Config schema above is the settings document the
28
+ * 0.1.7 Host serves for this row; the effective config arrives here as the
29
+ * `config` argument the Loader resolved and the Host reloads the row when the
30
+ * user saves a change, so nothing in this body observes or writes settings.
31
+ * @param ctx - host root context.
32
+ * @param config - the row's resolved configuration.
33
+ */
22
34
  export declare const apply: (ctx: Context, config?: Config) => void;