@aryacode/dsh-aryacode-polish 0.0.0-stage → 0.3.4
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.
- package/CHANGELOG.md +165 -0
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/client/account.js +96 -0
- package/client/face.js +403 -0
- package/client/money.js +31 -0
- package/client/switches.json +17 -0
- package/cordis.patch.yml +17 -0
- package/docs/parked-spellcheck.md +105 -0
- package/entries/config.js +61 -0
- package/entries/cost.js +273 -0
- package/entries/polish.js +57 -0
- package/entries/registry.js +73 -0
- package/entries/usage.js +265 -0
- package/lib/client.js +561 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +10 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +73 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aryacode/dsh-aryacode-polish — the browser face bundle.
|
|
3
|
+
*
|
|
4
|
+
* GENERATED FILE. Edit client/face.js or client/switches.json and run
|
|
5
|
+
* `pnpm build:client`; nothing here should be hand-edited.
|
|
6
|
+
*/
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: "@aryacode/dsh-aryacode-polish",
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
const SWITCHES = [
|
|
11
|
+
{
|
|
12
|
+
"key": "sessionCost",
|
|
13
|
+
"label": "Estimated session cost",
|
|
14
|
+
"description": "Show what the open session has cost so far, priced from recorded usage at the published DeepSeek rates.",
|
|
15
|
+
"defaultOn": true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"key": "accountBalance",
|
|
19
|
+
"label": "Account balance",
|
|
20
|
+
"description": "Show the remaining account balance in the sidebar foot, above the account row.",
|
|
21
|
+
"defaultOn": true
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
const ROW_ID = "polish"
|
|
25
|
+
const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
|
|
26
|
+
const CLIENT_VERSION = "0.3.4"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The two pure decisions behind the balance line: which wallet to show, and how
|
|
30
|
+
* to render it.
|
|
31
|
+
*
|
|
32
|
+
* They live in their own file because the browser face bundle is only ever run
|
|
33
|
+
* inside the Shell, where nothing can be imported and asserted — so the build
|
|
34
|
+
* inlines this module into `lib/client.js` (with the `export` keywords dropped)
|
|
35
|
+
* while `test/money.spec.mjs` imports it directly. What the test covers is
|
|
36
|
+
* therefore the same source the browser runs, not a copy of it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pick the wallet a balance line should show.
|
|
41
|
+
* @param view - the balance view the account Remote returned.
|
|
42
|
+
* @returns the first wallet carrying a balance, or undefined when the view has none.
|
|
43
|
+
*/
|
|
44
|
+
function walletOf(view) {
|
|
45
|
+
if (view === undefined || view === null || view.status !== 'ready' || !Array.isArray(view.value)) return undefined
|
|
46
|
+
return view.value.find(wallet => wallet !== null && typeof wallet === 'object' && typeof wallet.balance === 'string')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format one wallet as money.
|
|
51
|
+
* @param wallet - a wallet with `currency` and `balance`.
|
|
52
|
+
* @returns the amount with its currency symbol, at two decimals.
|
|
53
|
+
*/
|
|
54
|
+
function formatBalance(wallet) {
|
|
55
|
+
const symbol = wallet.currency === 'CNY' ? '\u00A5' : '$'
|
|
56
|
+
const amount = Number(wallet.balance)
|
|
57
|
+
return Number.isFinite(amount) ? symbol + amount.toFixed(2) : symbol + String(wallet.balance)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reading the account balance from a plugin face.
|
|
62
|
+
*
|
|
63
|
+
* The account Remote methods are gated, so the read runs inside a
|
|
64
|
+
* `remoteAccess.whenAccess('account/watch', …)` grant and the grant is released
|
|
65
|
+
* as soon as the read settles. Every failure — no service in this Shell, a
|
|
66
|
+
* refused grant, a failed call — resolves to `undefined` instead of throwing: a
|
|
67
|
+
* sidebar line is never worth breaking the shell over, and a line that stays
|
|
68
|
+
* empty is honest about a number it never received.
|
|
69
|
+
*
|
|
70
|
+
* This module is inlined into `lib/client.js` by `scripts/build-client.mjs` and
|
|
71
|
+
* imported directly by `test/account.spec.mjs`, so the tested source is the one
|
|
72
|
+
* the browser runs.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the account request identity for one balance read.
|
|
77
|
+
*
|
|
78
|
+
* The shipped account bundle inlines `DSH_CLIENT_VERSION` at build time; a plugin
|
|
79
|
+
* bundle has no such define, so the build stamps this package's own version and
|
|
80
|
+
* prefers the shell's value when one is inlined.
|
|
81
|
+
*
|
|
82
|
+
* @param version - the version this build stamps.
|
|
83
|
+
* @returns the metadata the account Remote methods carry.
|
|
84
|
+
*/
|
|
85
|
+
function clientMetadata(version) {
|
|
86
|
+
const stamped = typeof process !== 'undefined' && process.env !== undefined ? process.env.DSH_CLIENT_VERSION : undefined
|
|
87
|
+
return {
|
|
88
|
+
version: typeof stamped === 'string' && stamped !== '' ? stamped : version,
|
|
89
|
+
locale: typeof navigator !== 'undefined' && typeof navigator.language === 'string' && navigator.language !== ''
|
|
90
|
+
? navigator.language
|
|
91
|
+
: 'en',
|
|
92
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the one-shot balance reader.
|
|
98
|
+
*
|
|
99
|
+
* @param ctx - the browser plugin context; `ctx.get` resolves an optional service.
|
|
100
|
+
* @param version - the version this build stamps into the account metadata.
|
|
101
|
+
* @returns a reader that resolves to the balance view, or undefined when unavailable.
|
|
102
|
+
*/
|
|
103
|
+
function makeBalanceReader(ctx, version, { timeoutMs = 10000 } = {}) {
|
|
104
|
+
const service = name => {
|
|
105
|
+
try {
|
|
106
|
+
return ctx.get(name)
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return async () => {
|
|
112
|
+
const access = service('remoteAccess')
|
|
113
|
+
const remote = service('remote')
|
|
114
|
+
if (access === undefined || remote === undefined) return undefined
|
|
115
|
+
const metadata = clientMetadata(version)
|
|
116
|
+
return await new Promise(resolve => {
|
|
117
|
+
let settled = false
|
|
118
|
+
let stop
|
|
119
|
+
const timer = setTimeout(() => finish(undefined), timeoutMs)
|
|
120
|
+
const finish = value => {
|
|
121
|
+
if (settled) return
|
|
122
|
+
settled = true
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
resolve(value)
|
|
125
|
+
if (typeof stop === 'function') {
|
|
126
|
+
try {
|
|
127
|
+
stop()
|
|
128
|
+
} catch {
|
|
129
|
+
/* the grant may already have been released */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// The read starts before the yield so an effect runner that stops at the
|
|
134
|
+
// first yielded disposer still performs the call.
|
|
135
|
+
stop = access.whenAccess('account/watch', function* () {
|
|
136
|
+
void (async () => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await remote.account.getBalance(metadata)
|
|
139
|
+
const value = result !== undefined && result.ok === true ? result.value : undefined
|
|
140
|
+
finish(value !== undefined && value.status === 'ready' ? value : undefined)
|
|
141
|
+
} catch {
|
|
142
|
+
finish(undefined)
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
yield () => {
|
|
146
|
+
if (!settled) {
|
|
147
|
+
settled = true
|
|
148
|
+
clearTimeout(timer)
|
|
149
|
+
resolve(undefined)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* AryaCode Polish — the browser face.
|
|
159
|
+
*
|
|
160
|
+
* This file is deliberately not a module. `scripts/build-client.mjs` wraps it in
|
|
161
|
+
* the Shell's `__ModuleLoader__.load({ id, factory })` factory and prepends the
|
|
162
|
+
* switch list from `client/switches.json`, so `require` is the Shell's frozen
|
|
163
|
+
* module table and never Node's `require`. Keep `require(...)` limited to names
|
|
164
|
+
* the Shell answers; anything else must be inlined here.
|
|
165
|
+
*
|
|
166
|
+
* The Configure page is the point of this package: one boolean per feature, on
|
|
167
|
+
* by default, and the host half of a feature is disposed when its switch turns
|
|
168
|
+
* off. The page reads the row's live config form, so adding a feature needs no
|
|
169
|
+
* second registration here — one line in `client/switches.json` and a matching
|
|
170
|
+
* entry in `entries/registry.js`, and the tests fail if the two disagree.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
const React = require('react')
|
|
174
|
+
|
|
175
|
+
/** Shared spacing for the page's prose. */
|
|
176
|
+
const SPACING = { margin: '0 0 10px', opacity: 0.85, lineHeight: '1.5' }
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Read one switch out of a configuration object.
|
|
180
|
+
* @param value - the row's configuration object, when it has loaded.
|
|
181
|
+
* @param entry - the declared switch.
|
|
182
|
+
* @returns the boolean the switch holds, falling back to its declared default.
|
|
183
|
+
*/
|
|
184
|
+
function switchValue(value, entry) {
|
|
185
|
+
const current = value === undefined || value === null ? undefined : value[entry.key]
|
|
186
|
+
if (typeof current === 'boolean') return current
|
|
187
|
+
return entry.defaultOn !== false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Subscribe to one switch of this row's live configuration.
|
|
192
|
+
* @param configForms - the Shell's config-form service, or a getter for it.
|
|
193
|
+
* @param key - the switch to read.
|
|
194
|
+
* @param fallback - the value to use while the row's configuration has not loaded.
|
|
195
|
+
* @returns the switch's boolean value.
|
|
196
|
+
*/
|
|
197
|
+
function useSwitch(configForms, key, fallback) {
|
|
198
|
+
const resolved = typeof configForms === 'function' ? configForms() : configForms
|
|
199
|
+
const live = resolved === undefined ? undefined : resolved.get(ROW_ID)
|
|
200
|
+
const subscribe = React.useCallback(
|
|
201
|
+
listener => (live === undefined ? () => {} : live.subscribe(listener)),
|
|
202
|
+
[live],
|
|
203
|
+
)
|
|
204
|
+
const snapshot = React.useCallback(
|
|
205
|
+
() => (live === undefined ? undefined : live.getSnapshot()),
|
|
206
|
+
[live],
|
|
207
|
+
)
|
|
208
|
+
const state = React.useSyncExternalStore(subscribe, snapshot)
|
|
209
|
+
const value = state?.value
|
|
210
|
+
const current = value === undefined || value === null ? undefined : value[key]
|
|
211
|
+
return typeof current === 'boolean' ? current : fallback
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The row's Configure page and its one-line summary.
|
|
216
|
+
* @param props - the page's `view`, the Host's `form`, and our lazy `configForms`.
|
|
217
|
+
* @returns the page node, the summary node, or a status line.
|
|
218
|
+
*/
|
|
219
|
+
function PolishConfig(props) {
|
|
220
|
+
const { form, view } = props
|
|
221
|
+
|
|
222
|
+
// Resolved lazily: the Settings page's own client half may load after this face.
|
|
223
|
+
const configForms = typeof props.configForms === 'function' ? props.configForms() : props.configForms
|
|
224
|
+
const live = configForms === undefined ? undefined : configForms.get(ROW_ID)
|
|
225
|
+
const subscribe = React.useCallback(
|
|
226
|
+
listener => (live === undefined ? () => {} : live.subscribe(listener)),
|
|
227
|
+
[live],
|
|
228
|
+
)
|
|
229
|
+
const snapshot = React.useCallback(
|
|
230
|
+
() => (live === undefined ? form?.state : live.getSnapshot()),
|
|
231
|
+
[live, form],
|
|
232
|
+
)
|
|
233
|
+
const state = React.useSyncExternalStore(subscribe, snapshot)
|
|
234
|
+
const [draft, setDraft] = React.useState(null)
|
|
235
|
+
const [saving, setSaving] = React.useState(false)
|
|
236
|
+
const [notice, setNotice] = React.useState('')
|
|
237
|
+
|
|
238
|
+
const count = SWITCHES.length
|
|
239
|
+
const summary = count === 0 ? 'No switch registered yet.' : count === 1 ? '1 switch.' : count + ' switches.'
|
|
240
|
+
|
|
241
|
+
if (view === 'summary') return React.createElement('span', null, summary)
|
|
242
|
+
|
|
243
|
+
if (count === 0) {
|
|
244
|
+
return React.createElement('div', null,
|
|
245
|
+
React.createElement('p', { style: SPACING },
|
|
246
|
+
'No switch is registered yet. This page grows one checkbox per feature as features land; '
|
|
247
|
+
+ 'there is nothing here that can be toggled into a half-installed state.'),
|
|
248
|
+
React.createElement('p', { style: SPACING },
|
|
249
|
+
'A feature needs two lines: an entry in ',
|
|
250
|
+
React.createElement('code', null, 'entries/registry.js'),
|
|
251
|
+
' that declares its switch and host half, and a matching entry in ',
|
|
252
|
+
React.createElement('code', null, 'client/switches.json'),
|
|
253
|
+
' that names it here. The tests fail if the two disagree.'),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (state === undefined) {
|
|
258
|
+
return React.createElement('p', { style: SPACING }, 'This row does not expose its configuration to the Plugins page.')
|
|
259
|
+
}
|
|
260
|
+
const value = state.value
|
|
261
|
+
if (value === undefined) {
|
|
262
|
+
return React.createElement('p', { style: SPACING },
|
|
263
|
+
state.status === 'unavailable' ? 'This row has no configuration to change.' : 'Loading configuration…')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const current = draft === null ? value : draft
|
|
267
|
+
const dirty = SWITCHES.some(entry => switchValue(current, entry) !== switchValue(value, entry))
|
|
268
|
+
const writable = state.writable === true
|
|
269
|
+
const setSwitch = (key, next) => {
|
|
270
|
+
setDraft({ ...current, [key]: next })
|
|
271
|
+
setNotice('')
|
|
272
|
+
}
|
|
273
|
+
const save = async () => {
|
|
274
|
+
if (saving || !dirty) return
|
|
275
|
+
setSaving(true)
|
|
276
|
+
setNotice('')
|
|
277
|
+
try {
|
|
278
|
+
const ops = SWITCHES.map(entry => ({ op: 'set', path: [entry.key], value: switchValue(current, entry) }))
|
|
279
|
+
const owner = live === undefined ? form : live
|
|
280
|
+
const accepted = await owner.mutate(ops, state.revision)
|
|
281
|
+
if (accepted) {
|
|
282
|
+
setDraft(null)
|
|
283
|
+
setNotice('Saved. A feature switched off stops running now, not after a restart.')
|
|
284
|
+
} else {
|
|
285
|
+
setNotice('The Host refused the change.')
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
setNotice(error instanceof Error ? error.message : String(error))
|
|
289
|
+
}
|
|
290
|
+
setSaving(false)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return React.createElement('div', { style: { display: 'grid', gap: '16px' } },
|
|
294
|
+
React.createElement('p', { style: SPACING },
|
|
295
|
+
'Every switch is additive and on by default. Switching one off disposes that feature only — '
|
|
296
|
+
+ 'it stops costing the app instead of merely hiding.'),
|
|
297
|
+
React.createElement('div', { style: { display: 'grid', gap: '14px' } },
|
|
298
|
+
SWITCHES.map(entry => React.createElement('label', {
|
|
299
|
+
key: entry.key,
|
|
300
|
+
style: { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '10px', alignItems: 'start', cursor: writable ? 'pointer' : 'default' },
|
|
301
|
+
},
|
|
302
|
+
React.createElement('input', {
|
|
303
|
+
type: 'checkbox',
|
|
304
|
+
checked: switchValue(current, entry),
|
|
305
|
+
disabled: !writable || saving,
|
|
306
|
+
onChange: event => setSwitch(entry.key, event.target.checked),
|
|
307
|
+
}),
|
|
308
|
+
React.createElement('span', null,
|
|
309
|
+
React.createElement('span', { style: { display: 'block', fontWeight: 600 } }, entry.label),
|
|
310
|
+
React.createElement('span', { style: { display: 'block', opacity: 0.8, lineHeight: '1.45' } }, entry.description),
|
|
311
|
+
))),
|
|
312
|
+
),
|
|
313
|
+
React.createElement('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
|
|
314
|
+
React.createElement('button', {
|
|
315
|
+
type: 'button',
|
|
316
|
+
disabled: !writable || saving || !dirty,
|
|
317
|
+
onClick: save,
|
|
318
|
+
}, saving ? 'Saving…' : 'Save'),
|
|
319
|
+
React.createElement('span', { style: { opacity: 0.8 } },
|
|
320
|
+
notice !== '' ? notice : writable ? '' : 'This row is read-only in the current composition.')),
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Money from the wire view, which already did the arithmetic host-side. */
|
|
325
|
+
function formatMoney(value) {
|
|
326
|
+
const amount = Number(value)
|
|
327
|
+
if (!Number.isFinite(amount) || amount <= 0) return ''
|
|
328
|
+
// Pennies matter at the top and single tokens matter at the bottom: a small
|
|
329
|
+
// turn really does cost a fraction of a cent, and "$0" would hide it.
|
|
330
|
+
const fixed = amount >= 1 ? amount.toFixed(2)
|
|
331
|
+
: amount >= 0.01 ? amount.toFixed(3)
|
|
332
|
+
: amount.toPrecision(2)
|
|
333
|
+
const text = fixed.includes('e') ? amount.toFixed(8) : fixed
|
|
334
|
+
return '$' + text.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Token counts, grouped, the way the harness pill next to ours reads. */
|
|
338
|
+
function formatTokens(value) {
|
|
339
|
+
const count = Number(value)
|
|
340
|
+
return Number.isFinite(count) ? new Intl.NumberFormat('en-US').format(count) + ' tok' : ''
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** One label/value line of the breakdown card. */
|
|
344
|
+
function detailRow(label, value, key) {
|
|
345
|
+
return React.createElement('div', {
|
|
346
|
+
key,
|
|
347
|
+
style: { display: 'flex', justifyContent: 'space-between', gap: '16px', padding: '2px 0' },
|
|
348
|
+
},
|
|
349
|
+
React.createElement('span', { style: { opacity: 0.75 } }, label),
|
|
350
|
+
React.createElement('span', null, value))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The small text under the card that keeps the number honest. */
|
|
354
|
+
const COST_FOOTNOTE = 'Based on recorded usage and published rates. Your provider\'s bill is authoritative.'
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The composer-dock pill, and the breakdown card it opens.
|
|
358
|
+
* Reads the priced projection the Host registers, so no rate table or money
|
|
359
|
+
* arithmetic runs in the browser.
|
|
360
|
+
* @param props - standard session props plus our lazy `configForms`.
|
|
361
|
+
* @returns the pill, or nothing while the feature is off or the session has no usage.
|
|
362
|
+
*/
|
|
363
|
+
function CostPill(props) {
|
|
364
|
+
const switched = useSwitch(props.configForms, 'sessionCost', true)
|
|
365
|
+
const usage = typeof props.useProjection === 'function' ? props.useProjection('polishCost') : undefined
|
|
366
|
+
const [open, setOpen] = React.useState(false)
|
|
367
|
+
const root = React.useRef(null)
|
|
368
|
+
|
|
369
|
+
React.useEffect(() => {
|
|
370
|
+
if (!open) return undefined
|
|
371
|
+
const close = event => {
|
|
372
|
+
if (root.current !== null && !root.current.contains(event.target)) setOpen(false)
|
|
373
|
+
}
|
|
374
|
+
document.addEventListener('pointerdown', close)
|
|
375
|
+
return () => document.removeEventListener('pointerdown', close)
|
|
376
|
+
}, [open])
|
|
377
|
+
|
|
378
|
+
if (!switched) return null
|
|
379
|
+
const tokens = typeof usage?.tokens === 'number' ? usage.tokens : 0
|
|
380
|
+
if (tokens === 0) return null
|
|
381
|
+
const money = formatMoney(usage.costUsd)
|
|
382
|
+
const label = money === '' ? 'Not priced' : (usage.incomplete ? '≈' + money + ' partially priced' : '≈' + money)
|
|
383
|
+
const cacheHit = tokens === 0 ? 0 : Math.round((usage.cacheRead ?? 0) / tokens * 100)
|
|
384
|
+
const models = Array.isArray(usage.models) ? usage.models : []
|
|
385
|
+
|
|
386
|
+
const modelName = models.length === 0 ? 'unknown'
|
|
387
|
+
: models.length === 1 ? models[0].model
|
|
388
|
+
: models.length + ' models'
|
|
389
|
+
const providerName = models.length === 0 ? 'unknown'
|
|
390
|
+
: models.length === 1 ? models[0].provider
|
|
391
|
+
: models.length + ' providers'
|
|
392
|
+
const segment = (value, shade, key) => {
|
|
393
|
+
const countValue = Number(value ?? 0)
|
|
394
|
+
if (!Number.isFinite(countValue) || countValue <= 0) return null
|
|
395
|
+
return React.createElement('div', {
|
|
396
|
+
key,
|
|
397
|
+
style: { flexGrow: countValue, background: 'color-mix(in srgb, CanvasText ' + shade + '%, transparent)' },
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
const bar = React.createElement('div', {
|
|
401
|
+
style: {
|
|
402
|
+
display: 'flex',
|
|
403
|
+
height: '6px',
|
|
404
|
+
margin: '10px 0 12px',
|
|
405
|
+
borderRadius: '999px',
|
|
406
|
+
overflow: 'hidden',
|
|
407
|
+
background: 'color-mix(in srgb, CanvasText 10%, transparent)',
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
segment(usage.cacheRead, 22, 'bar-cached'),
|
|
411
|
+
segment(usage.uncached, 48, 'bar-uncached'),
|
|
412
|
+
segment(usage.cacheWrite, 62, 'bar-cache-write'),
|
|
413
|
+
segment(usage.output, 82, 'bar-output'))
|
|
414
|
+
|
|
415
|
+
return React.createElement('div', {
|
|
416
|
+
ref: root,
|
|
417
|
+
style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
|
|
418
|
+
},
|
|
419
|
+
React.createElement('button', {
|
|
420
|
+
type: 'button',
|
|
421
|
+
onClick: () => setOpen(!open),
|
|
422
|
+
title: 'Estimated session cost',
|
|
423
|
+
style: {
|
|
424
|
+
background: 'transparent',
|
|
425
|
+
border: 'none',
|
|
426
|
+
color: 'inherit',
|
|
427
|
+
cursor: 'pointer',
|
|
428
|
+
font: 'inherit',
|
|
429
|
+
padding: '0 2px',
|
|
430
|
+
opacity: 0.8,
|
|
431
|
+
},
|
|
432
|
+
}, label),
|
|
433
|
+
open
|
|
434
|
+
? React.createElement('div', {
|
|
435
|
+
style: {
|
|
436
|
+
position: 'absolute',
|
|
437
|
+
bottom: '100%',
|
|
438
|
+
left: 0,
|
|
439
|
+
marginBottom: '8px',
|
|
440
|
+
width: '320px',
|
|
441
|
+
padding: '14px',
|
|
442
|
+
borderRadius: '10px',
|
|
443
|
+
border: '1px solid color-mix(in srgb, CanvasText 16%, transparent)',
|
|
444
|
+
background: 'Canvas',
|
|
445
|
+
color: 'CanvasText',
|
|
446
|
+
boxShadow: '0 10px 30px color-mix(in srgb, CanvasText 18%, transparent)',
|
|
447
|
+
fontSize: '12px',
|
|
448
|
+
lineHeight: '1.5',
|
|
449
|
+
zIndex: 40,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
React.createElement('div', {
|
|
453
|
+
style: { display: 'flex', justifyContent: 'space-between', gap: '16px', fontWeight: 500 },
|
|
454
|
+
},
|
|
455
|
+
React.createElement('span', null, 'Estimated session cost'),
|
|
456
|
+
React.createElement('span', null, label)),
|
|
457
|
+
bar,
|
|
458
|
+
detailRow('Cache hit', cacheHit + '%', 'cache-hit'),
|
|
459
|
+
detailRow('Uncached input', formatTokens(usage.uncached), 'uncached'),
|
|
460
|
+
detailRow('Cached input', formatTokens(usage.cacheRead), 'cached'),
|
|
461
|
+
(usage.cacheWrite ?? 0) > 0 ? detailRow('Cache write', formatTokens(usage.cacheWrite), 'cache-write') : null,
|
|
462
|
+
detailRow('Output', formatTokens(usage.output), 'output'),
|
|
463
|
+
detailRow('Model', modelName, 'model'),
|
|
464
|
+
detailRow('Provider', providerName, 'provider'),
|
|
465
|
+
detailRow('Requests', String(usage.requests ?? 0), 'requests'),
|
|
466
|
+
React.createElement('div', { style: { borderTop: '1px solid color-mix(in srgb, CanvasText 12%, transparent)', margin: '8px 0' } }),
|
|
467
|
+
detailRow('Estimated session cost', money === '' ? 'Not priced' : money, 'cost'),
|
|
468
|
+
React.createElement('p', { style: { margin: '10px 0 0', opacity: 0.7, fontSize: '11px' } }, COST_FOOTNOTE))
|
|
469
|
+
: null)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The remaining-balance line in the sidebar foot.
|
|
474
|
+
*
|
|
475
|
+
* It renders nothing at all when the switch is off, the account exposes no
|
|
476
|
+
* wallet, or the read was refused — an empty line is better than a line that
|
|
477
|
+
* claims a number the Host did not give us.
|
|
478
|
+
*
|
|
479
|
+
* @param props - the slot's `wide` flag, our lazy `configForms`, and the balance reader.
|
|
480
|
+
* @returns the balance line, or null.
|
|
481
|
+
*/
|
|
482
|
+
function BalanceLine(props) {
|
|
483
|
+
const enabled = useSwitch(props.configForms, 'accountBalance', true)
|
|
484
|
+
const [wallet, setWallet] = React.useState(undefined)
|
|
485
|
+
const read = props.loadBalance
|
|
486
|
+
React.useEffect(() => {
|
|
487
|
+
if (!enabled || typeof read !== 'function') return undefined
|
|
488
|
+
let live = true
|
|
489
|
+
Promise.resolve()
|
|
490
|
+
.then(() => read())
|
|
491
|
+
.then(view => {
|
|
492
|
+
if (live) setWallet(walletOf(view))
|
|
493
|
+
}, () => {
|
|
494
|
+
if (live) setWallet(undefined)
|
|
495
|
+
})
|
|
496
|
+
return () => {
|
|
497
|
+
live = false
|
|
498
|
+
}
|
|
499
|
+
}, [enabled, read])
|
|
500
|
+
if (!enabled) return null
|
|
501
|
+
// The wrapper stays in the DOM even when there is no number to show, so a
|
|
502
|
+
// reader (and the verification script) can tell "registered but empty" from
|
|
503
|
+
// "not registered at all". An empty flex line costs the layout nothing.
|
|
504
|
+
if (wallet === undefined) {
|
|
505
|
+
return React.createElement('div', { 'data-polish-balance': 'empty', style: { display: 'none' } })
|
|
506
|
+
}
|
|
507
|
+
const money = formatBalance(wallet)
|
|
508
|
+
return React.createElement('div', {
|
|
509
|
+
title: 'Remaining account balance',
|
|
510
|
+
style: {
|
|
511
|
+
display: 'flex',
|
|
512
|
+
alignItems: 'center',
|
|
513
|
+
gap: '6px',
|
|
514
|
+
padding: '2px 8px 4px',
|
|
515
|
+
fontSize: '12px',
|
|
516
|
+
lineHeight: '16px',
|
|
517
|
+
minWidth: 0,
|
|
518
|
+
overflow: 'hidden',
|
|
519
|
+
whiteSpace: 'nowrap',
|
|
520
|
+
textOverflow: 'ellipsis',
|
|
521
|
+
opacity: 0.75,
|
|
522
|
+
},
|
|
523
|
+
}, props.wide === false ? money : 'Balance ' + money)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
inject: ['slots'],
|
|
528
|
+
/**
|
|
529
|
+
* Register this bundle's surfaces.
|
|
530
|
+
* @param ctx - the browser plugin context.
|
|
531
|
+
*/
|
|
532
|
+
apply(ctx) {
|
|
533
|
+
const configForms = () => {
|
|
534
|
+
try {
|
|
535
|
+
return ctx.get('configForms')
|
|
536
|
+
} catch {
|
|
537
|
+
return undefined
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
541
|
+
name: 'plugins.row.config',
|
|
542
|
+
key: ROW_KEY,
|
|
543
|
+
inject: () => ({ configForms }),
|
|
544
|
+
}, PolishConfig))
|
|
545
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
546
|
+
name: 'conversation.composer.dock',
|
|
547
|
+
id: 'polish-cost',
|
|
548
|
+
order: 2,
|
|
549
|
+
inject: () => ({ configForms }),
|
|
550
|
+
}, CostPill))
|
|
551
|
+
const loadBalance = makeBalanceReader(ctx, CLIENT_VERSION)
|
|
552
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
553
|
+
name: 'sidebar.footer.action',
|
|
554
|
+
id: 'polish-balance',
|
|
555
|
+
order: 3,
|
|
556
|
+
inject: () => ({ configForms, loadBalance }),
|
|
557
|
+
}, BalanceLine))
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
})
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of AryaCode Polish.
|
|
3
|
+
*
|
|
4
|
+
* The row in `cordis.patch.yml` names this package and not a subpath, because a
|
|
5
|
+
* package registers exactly one client bundle and the Shell resolves it from the
|
|
6
|
+
* exact package specifier the mounted row names. The host work itself lives in
|
|
7
|
+
* `entries/polish.js` — a registry of features, each gated by one volatile
|
|
8
|
+
* switch — so this module exists to be that mount point and re-exports the row.
|
|
9
|
+
*/
|
|
10
|
+
export { Config, apply, features, inject, name } from '../entries/polish.js'
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"cordis",
|
|
9
|
+
"plugin",
|
|
10
|
+
"bundle",
|
|
11
|
+
"polish",
|
|
12
|
+
"quality-of-life",
|
|
13
|
+
"cost",
|
|
14
|
+
"usage"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/KonnectSuite/dsh-aryacode-polish.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/KonnectSuite/dsh-aryacode-polish#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/KonnectSuite/dsh-aryacode-polish/issues"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"entries/**/*",
|
|
32
|
+
"client/**/*",
|
|
33
|
+
"lib/**/*",
|
|
34
|
+
"locale/*.json",
|
|
35
|
+
"docs/**/*",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"dsh": "^0.2.1-rc.3"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build:client": "node scripts/build-client.mjs",
|
|
53
|
+
"prepack": "node scripts/build-client.mjs",
|
|
54
|
+
"test": "node --test test/patch.spec.mjs test/config.spec.mjs test/registry.spec.mjs test/cost.spec.mjs test/money.spec.mjs test/account.spec.mjs test/docs.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"zod": "^4.4.3"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
64
|
+
"js-yaml": "^4.1.0"
|
|
65
|
+
},
|
|
66
|
+
"exports": {
|
|
67
|
+
".": {
|
|
68
|
+
"types": "./lib/index.d.ts",
|
|
69
|
+
"default": "./lib/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./package.json": "./package.json",
|
|
72
|
+
"./client": "./lib/client.js",
|
|
73
|
+
"./polish": "./entries/polish.js"
|
|
74
|
+
}
|
|
75
|
+
}
|