@linxin666/dsh-usage 0.4.9 → 0.4.11

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.
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Form model behind the plugin settings card. Every control applies itself:
3
+ * a pick (a segment, a select option, a reset) writes the moment it is made,
4
+ * and typed text writes once typing pauses or the reader leaves the field.
5
+ * Between the edit and the write the edit is held here as a draft, so what is
6
+ * on screen is exactly what the write will store; a write the Host refuses
7
+ * puts a pick back to the stored value and keeps typed text for correcting.
8
+ * Family-shared slice inlined into each plugin's client bundle.
9
+ */
10
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
11
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
12
+ /** The write one field's draft text performs when it is applied. */
13
+ export type FieldWrite = {
14
+ kind: 'set';
15
+ value: unknown;
16
+ } | {
17
+ kind: 'clear';
18
+ };
19
+ /** How one field converts between its stored value and its draft text. */
20
+ export interface FieldSpec {
21
+ /** Field name inside the namespace section. */
22
+ field: string;
23
+ /**
24
+ * How the reader enters a value, which decides when it is written: a `pick`
25
+ * is a whole value at once and applies immediately; `type` is text arriving
26
+ * a keystroke at a time and applies once typing pauses or the field is left.
27
+ */
28
+ entry: 'pick' | 'type';
29
+ /**
30
+ * Whether the Host treats this field as a secret and redacts its value from
31
+ * the read-back (role('secret') in the section schema). Redacted secrets are
32
+ * never compared against the draft on save: the Host strips them from every
33
+ * wire view layer, so the settled snapshot carries nothing to read back. A
34
+ * staged secret set is judged by the mutation settling; the rest of its
35
+ * batch, when one exists, is still judged by read-back, and the atomic
36
+ * mutation lands every write or none.
37
+ */
38
+ secret?: boolean;
39
+ /** Render a stored value as draft text; the empty string when the section carries none. */
40
+ format: (value: unknown) => string;
41
+ /**
42
+ * The write this draft text performs, or undefined when the text is not a
43
+ * value this field accepts — which holds the draft unwritten rather than
44
+ * discarding it.
45
+ */
46
+ parse: (text: string) => FieldWrite | undefined;
47
+ }
48
+ /** One field as the card renders it. */
49
+ export interface FieldState {
50
+ /** Draft text the control renders. */
51
+ text: string;
52
+ /** Whether the field leaves (or will leave) a user-layer entry. */
53
+ overridden: boolean;
54
+ /** Whether the draft is not a value this field accepts, which holds it unwritten. */
55
+ invalid: boolean;
56
+ }
57
+ /** Form state every plugin settings card shares. */
58
+ export interface CardShell {
59
+ /** False while the namespace is still loading; the card renders nothing. */
60
+ available: boolean;
61
+ /**
62
+ * Whether the namespace is actually served to this client. False when the
63
+ * Host deployment does not expose it (e.g. the owning plugin's settings
64
+ * domain is not mounted): the card renders an explanation
65
+ * instead of its form, so a missing namespace never looks like a missing
66
+ * plugin.
67
+ */
68
+ exposed: boolean;
69
+ /** Whether the Host document accepts writes. */
70
+ writable: boolean;
71
+ /** Whether the form holds drafts not yet written (typed text waiting for its pause). */
72
+ dirty: boolean;
73
+ /** Whether any draft is invalid, which holds it unwritten. */
74
+ invalid: boolean;
75
+ /** Whether a write is crossing the wire. */
76
+ saving: boolean;
77
+ /** Whether the last write did not land; cleared by the next edit or write. */
78
+ failed: boolean;
79
+ /**
80
+ * The rejection code/message the Host returned for the last failed save,
81
+ * surfaced next to the generic failure text. Undefined while no save has
82
+ * failed (or the failure carried no server reason).
83
+ */
84
+ failedReason?: string;
85
+ }
86
+ /** The write actions the card's slot entry injects. */
87
+ export interface CardActions {
88
+ /** Take draft text for one field; a pick writes at once, typed text after a pause. */
89
+ edit: (field: string, text: string) => void;
90
+ /** Clear one field at once, so it re-inherits the composition layer. */
91
+ resetField: (field: string) => void;
92
+ /** Write every pending draft now (a text field's Enter or blur), then re-seed from what the Host accepted. */
93
+ save: () => void;
94
+ /** Drop every pending draft, putting each control back to the stored value (a text field's Escape). */
95
+ discard: () => void;
96
+ }
97
+ /** How long typed text rests before it is written. */
98
+ export declare const TYPE_PAUSE_MS = 600;
99
+ /** One durable write inside the save's atomic form mutation. */
100
+ export interface BatchedWrite {
101
+ /** Field this entry writes. */
102
+ field: string;
103
+ /** set stores a value; unset drops the leaf. */
104
+ op: 'set' | 'unset';
105
+ /** Value for op set (absent for unset). */
106
+ value?: unknown;
107
+ }
108
+ /** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
109
+ export interface NumberConstraints {
110
+ /** The accepted value must be a whole number. */
111
+ integer?: boolean;
112
+ /** The accepted value must be at least this. */
113
+ min?: number;
114
+ }
115
+ /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints stays unwritten. */
116
+ export declare function numberField(field: string, constraints?: NumberConstraints): FieldSpec;
117
+ /**
118
+ * A free-text field. An empty draft clears the field. `entry` is `pick` when
119
+ * the text arrives whole from a picker (a preset or a reasoning level).
120
+ */
121
+ export declare function textField(field: string, entry?: FieldSpec['entry']): FieldSpec;
122
+ /**
123
+ * A free-text field the Host treats as a secret and redacts from the read-back
124
+ * (role('secret') in the section schema). The card still edits it like text,
125
+ * but a save never compares the redacted value back: the staged set is judged
126
+ * by the mutation settling (see {@link FieldSpec.secret}).
127
+ */
128
+ export declare function secretField(field: string): FieldSpec;
129
+ /** A boolean field, edited through true/false draft text. */
130
+ export declare function booleanField(field: string): FieldSpec;
131
+ /** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
132
+ export declare function choiceField(field: string, choices: readonly string[]): FieldSpec;
133
+ /**
134
+ * Applies one card's edits to one settings namespace as the reader makes them.
135
+ *
136
+ * The Host is the only authority on whether a value was accepted — its
137
+ * validators own the constraints no schema can express — so the outcome is
138
+ * read back from the section rather than predicted here. A write that did not
139
+ * land puts each pick back to the stored value, the way a switch that could not
140
+ * flip springs back, and keeps typed text so the reader can correct it instead
141
+ * of retyping.
142
+ */
143
+ export declare class CardForm<T> {
144
+ private readonly scope;
145
+ private readonly specs;
146
+ private readonly staged;
147
+ private readonly listeners;
148
+ /** The form subscription installed in the constructor; released by dispose(). */
149
+ private readonly disposeForm;
150
+ private disposed;
151
+ /** The pending write of typed text, waiting for typing to pause. */
152
+ private typePause;
153
+ private saving;
154
+ /** A save asked for while one was in flight; it runs once the first settles (#1754). */
155
+ private saveQueued;
156
+ private failed;
157
+ private failedReason;
158
+ /** @param scope - the bound configuration form for this card's namespace. */
159
+ constructor(scope: ConfigForm<T>, specs: FieldSpec[]);
160
+ /**
161
+ * Release the form subscription and every bound store listener. The card
162
+ * must call this on teardown; later calls are no-ops.
163
+ */
164
+ dispose(): void;
165
+ /** Publish a projection of this form, rebuilt whenever the form or a draft changes. */
166
+ bind<S>(project: () => S): SnapshotStore<S>;
167
+ /** Read the card-level state: what the Host serves, and what is still waiting to be written. */
168
+ shell(): CardShell;
169
+ /** Read one field's state from the effective section and its staged draft. */
170
+ field(field: string): FieldState;
171
+ /** The actions the card's slot registration injects. */
172
+ actions(): CardActions;
173
+ /**
174
+ * Run one write, and re-run it once if another was asked for while this one
175
+ * was still in flight.
176
+ *
177
+ * A write is a Host round trip that also drives the profile reconcile, and
178
+ * the Host runs it inside one exclusive transaction. Answering an edit that
179
+ * arrives mid-flight by returning immediately dropped it with no explanation
180
+ * (#1754). Serializing instead means an edit made while another write is
181
+ * still settling is written against the settled state.
182
+ * @returns settlement after the mutation and the read-back.
183
+ */
184
+ requestSave(): Promise<void>;
185
+ /**
186
+ * Write every valid draft in one atomic form mutation, then re-seed from
187
+ * what the Host accepted. A draft the field does not accept stays on screen,
188
+ * unwritten, without holding the others back.
189
+ *
190
+ * The batch rides one mutate, so cross-field validate hooks (baseURL+model)
191
+ * judge it as a unit: the Host either applies every write or refuses the
192
+ * batch. The form contract answers a refusal or a skipped write with `false`
193
+ * (it recovers with a fresh Host view instead of throwing), so the outcome is
194
+ * judged twice: the answer itself, and then the settled snapshot read back
195
+ * one planned write at a time. One missed write fails the whole batch. A
196
+ * transport that rejects instead (the dsh-web bridge controller on a dead
197
+ * connection) reports through the same failure path with its rejection
198
+ * message. A batch that did not land puts its picks and clears back to the
199
+ * stored value and keeps its typed text, so the reader can correct it.
200
+ * @returns settlement after the mutation and the read-back.
201
+ */
202
+ save(): Promise<void>;
203
+ /**
204
+ * Every staged edit a write would carry. An entry whose draft is not a value
205
+ * its field accepts carries no write: the form is still dirty, and the draft
206
+ * stays on screen rather than being dropped. A staged edit that matches the
207
+ * effective section is not a write at all.
208
+ * @returns the planned writes, in the order the fields were staged.
209
+ */
210
+ private plan;
211
+ /**
212
+ * Read-back judgment for a planned set: the user layer must hold the
213
+ * intended value once the mutation has settled.
214
+ */
215
+ private landedSet;
216
+ /**
217
+ * Read-back judgment for a planned unset: the field must be gone from the
218
+ * user layer once the mutation has settled.
219
+ */
220
+ private landedUnset;
221
+ /**
222
+ * Take one edit and schedule its write: a pick or a clear is written now,
223
+ * typed text once typing has rested for TYPE_PAUSE_MS.
224
+ */
225
+ private stage;
226
+ /** Write every pending draft now, taking the place of a waiting pause. */
227
+ private applyNow;
228
+ private specOf;
229
+ private snapshotOf;
230
+ private sectionValue;
231
+ private baseValue;
232
+ private userLayer;
233
+ private stored;
234
+ private publish;
235
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The usage page's settings card: the plugin switch and the three display
3
+ * rows, written through the family card form so a pick lands at once and a
4
+ * typed value after a pause, exactly as the other Web Extensions pages do.
5
+ * @module @linxin666/dsh-usage/client/usage-settings-card
6
+ */
7
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
8
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
9
+ import { type CardActions, type CardShell, type FieldState } from './settings-form.ts';
10
+ /** The settings fields this page edits (the namespace's own schema). */
11
+ export interface UsageSettings {
12
+ enabled?: boolean;
13
+ pollIntervalSec?: number;
14
+ /** Currency the spend figures are displayed in; the ledger stays CNY. */
15
+ spendCurrency?: 'CNY' | 'USD';
16
+ /** CNY per USD, consulted only while the display currency is USD. */
17
+ cnyPerUsd?: number;
18
+ }
19
+ /** The display currencies the spend figures offer. */
20
+ export declare const SPEND_CURRENCIES: readonly ["CNY", "USD"];
21
+ /** Bounds of the poll interval, mirroring the host schema; the slider walks them in steps of 10 s. */
22
+ export declare const POLL_INTERVAL_RANGE: {
23
+ min: number;
24
+ max: number;
25
+ step: number;
26
+ };
27
+ /** What the settings card renders. */
28
+ export interface UsageSettingsCardState extends CardShell {
29
+ enabled: FieldState;
30
+ pollIntervalSec: FieldState;
31
+ spendCurrency: FieldState;
32
+ cnyPerUsd: FieldState;
33
+ }
34
+ /** The card's snapshot and its form actions, as the section receives them. */
35
+ export interface UsageSettingsCardFace extends CardActions {
36
+ store: SnapshotStore<UsageSettingsCardState>;
37
+ }
38
+ /** Bridges the usage namespace onto the family card form for the plugin's lifetime. */
39
+ export declare class UsageSettingsCardController {
40
+ private readonly form;
41
+ private readonly store;
42
+ /** @param scope - the bound configuration form of the usage namespace. */
43
+ constructor(scope: ConfigForm<UsageSettings>);
44
+ /** The face the section's slot entry injects. */
45
+ inject(): UsageSettingsCardFace;
46
+ /** Release the form subscription; the plugin disposer calls this on teardown. */
47
+ dispose(): void;
48
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One settings page's seat in either the Web Extensions parent or the legacy
3
+ * first-level settings navigation.
4
+ *
5
+ * The parent supplies the page contract; a child does not know which slot the
6
+ * parent uses internally. While `webUiExtensions` is live the page is handed
7
+ * to its service, which owns keyed mounting and disposal. Without the parent,
8
+ * the same component registers as its original first-level settings section,
9
+ * so a standalone install keeps a working entry.
10
+ */
11
+ /** The parent service face this helper consumes. */
12
+ export interface WebExtensionPageService {
13
+ readonly sectionId: string;
14
+ registerPage(page: WebExtensionPageSeat): () => void;
15
+ }
16
+ /** Seat-independent page contribution. */
17
+ export interface WebExtensionPageSeat {
18
+ key: string;
19
+ order: number;
20
+ locale: string;
21
+ label(): string;
22
+ inject(): object;
23
+ component: unknown;
24
+ children?: Record<string, {
25
+ kind: 'keyed' | 'list';
26
+ scope: 'root';
27
+ }>;
28
+ }
29
+ /** The slot-registry slice used for the standalone fallback. */
30
+ export interface WebExtensionPageSlots {
31
+ register(options: never, component: never): unknown;
32
+ spec?(key: string): unknown;
33
+ entries?(key: string): readonly {
34
+ component?: unknown;
35
+ options: Record<string, unknown>;
36
+ }[];
37
+ }
38
+ /** The context slice needed by the migration. */
39
+ export interface WebExtensionPageContext {
40
+ slots: WebExtensionPageSlots;
41
+ get?(name: string): unknown;
42
+ on?(event: string, listener: () => void): unknown;
43
+ }
44
+ /**
45
+ * Keep exactly one reachable page contribution while the parent loads or
46
+ * unloads. The parent service owns integrated mounting; only the fallback uses
47
+ * this plugin's own slot registry. Slot disposal is synchronous, so the latch
48
+ * prevents a move from recursively registering a duplicate.
49
+ */
50
+ export declare function installWebExtensionPage(ctx: WebExtensionPageContext, page: WebExtensionPageSeat): () => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-usage",
3
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 compact price glance card above the sidebar's Settings row",
4
- "version": "0.4.9",
4
+ "version": "0.4.11",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "engines": {