@aryacode/dsh-aryacode-polish 0.2.4 → 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 +69 -0
- package/README.md +3 -0
- package/client/account.js +96 -0
- package/client/face.js +61 -0
- package/client/money.js +31 -0
- package/client/switches.json +6 -0
- package/entries/registry.js +6 -0
- package/lib/client.js +197 -0
- package/package.json +10 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4] - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **The bundle says which Harness it runs on.** `dsh.engines.dsh` is now declared
|
|
8
|
+
(`^0.2.1-rc.3`, the revision the two surfaces were verified against), so the
|
|
9
|
+
market can read the requirement instead of labelling the card "Host requirement
|
|
10
|
+
unknown", and an install onto an older Host can be refused rather than failing
|
|
11
|
+
at mount time. `test/manifests.spec.mjs` keeps the field present and semver.
|
|
12
|
+
|
|
13
|
+
## [0.3.3] - 2026-10-27
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The balance read could wait forever.** When the Host withheld the
|
|
18
|
+
`account/watch` grant, the read's promise never settled: the callback never
|
|
19
|
+
ran, so nothing resolved and the line silently stayed empty for the life of the
|
|
20
|
+
page. The read now carries a timeout and also starts its call before the
|
|
21
|
+
effect's yielded disposer, so an effect runner that stops at the first yield
|
|
22
|
+
still performs it. `test/account.spec.mjs` pins all seven refusal paths.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The read itself moved into `client/account.js`, beside `client/money.js`, for
|
|
27
|
+
the same reason: the build inlines it into the browser bundle and the spec
|
|
28
|
+
imports the same source.
|
|
29
|
+
|
|
30
|
+
## [0.3.2] - 2026-10-27
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **The balance decisions are testable.** Which wallet a line shows, and how the
|
|
35
|
+
amount is rendered (currency symbol, two decimals, unparseable values shown
|
|
36
|
+
verbatim), moved into `client/money.js`. The build inlines that file into
|
|
37
|
+
`lib/client.js`, so `test/money.spec.mjs` covers the exact source the browser
|
|
38
|
+
runs instead of a copy.
|
|
39
|
+
|
|
40
|
+
## [0.3.1] - 2026-10-27
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **The balance line had no observable mount point.** When the account exposed no
|
|
45
|
+
wallet it rendered `null`, which is correct for the user but indistinguishable
|
|
46
|
+
from "the surface was never registered" for anyone verifying the build. The
|
|
47
|
+
component now always renders its wrapper (`data-polish-balance="empty"`, hidden)
|
|
48
|
+
and fills it with the amount only when the Host returns one.
|
|
49
|
+
|
|
50
|
+
## [0.3.0] - 2026-10-27
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **Remaining account balance in the sidebar foot.** A line above the account row
|
|
55
|
+
shows the account's own wallet (`$17.33`), read once through a
|
|
56
|
+
`remoteAccess.whenAccess('account/watch', …)` grant so the call is allowed and
|
|
57
|
+
the grant is released as soon as the read settles. It renders nothing at all
|
|
58
|
+
when the switch is off, when the Host exposes no wallet, or when the read is
|
|
59
|
+
refused — an empty line beats a number the Host did not give us.
|
|
60
|
+
- **A second switch, `accountBalance`.** It is declared once in
|
|
61
|
+
`entries/registry.js` and once in `client/switches.json`, exactly like the cost
|
|
62
|
+
switch, so the Configure page grows it automatically.
|
|
63
|
+
|
|
64
|
+
### Notes
|
|
65
|
+
|
|
66
|
+
- The line sits in `sidebar.footer.action`, the one documented seat in the
|
|
67
|
+
sidebar foot. It renders *above* the account row, because
|
|
68
|
+
`SidebarRoot.tsx:405-411` puts `footerActions` before `settingsArea`; the only
|
|
69
|
+
seat below the login would be `settings.launcher`, which means shadowing the
|
|
70
|
+
account row and its whole menu.
|
|
71
|
+
|
|
3
72
|
## [0.2.4] - 2026-10-27
|
|
4
73
|
|
|
5
74
|
### Changed
|
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ Configure page automatically.
|
|
|
30
30
|
| Switch | Default | What it does |
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| **Estimated session cost** (`sessionCost`) | On | A `≈$0.39` pill in the composer dock that opens a breakdown card — cache hit, uncached input, cached input, output, requests, and the estimate — priced from the session's recorded usage at the published DeepSeek rates. |
|
|
33
|
+
| **Account balance** (`accountBalance`) | On | The account's remaining wallet (`$17.33`) in the sidebar foot, above the account row. Renders nothing when the Host exposes no wallet, so it never shows a number nobody gave it. |
|
|
33
34
|
|
|
34
35
|
### How the cost is computed
|
|
35
36
|
|
|
@@ -91,6 +92,8 @@ node --import tsx/esm apps/cli/src/bin.ts plugin --profile web add <path-to-tgz>
|
|
|
91
92
|
|
|
92
93
|
## Requirements
|
|
93
94
|
|
|
95
|
+
- AryaCode `^0.2.1-rc.3` (declared as `dsh.engines.dsh`; the revision both
|
|
96
|
+
surfaces were verified against).
|
|
94
97
|
- AryaCode with the Cordis plugin API (`@deepseek-ai/cordis ~4.0.4`).
|
|
95
98
|
- Node `^22.19.0 || >=24.0.0` to build from source.
|
|
96
99
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the account balance from a plugin face.
|
|
3
|
+
*
|
|
4
|
+
* The account Remote methods are gated, so the read runs inside a
|
|
5
|
+
* `remoteAccess.whenAccess('account/watch', …)` grant and the grant is released
|
|
6
|
+
* as soon as the read settles. Every failure — no service in this Shell, a
|
|
7
|
+
* refused grant, a failed call — resolves to `undefined` instead of throwing: a
|
|
8
|
+
* sidebar line is never worth breaking the shell over, and a line that stays
|
|
9
|
+
* empty is honest about a number it never received.
|
|
10
|
+
*
|
|
11
|
+
* This module is inlined into `lib/client.js` by `scripts/build-client.mjs` and
|
|
12
|
+
* imported directly by `test/account.spec.mjs`, so the tested source is the one
|
|
13
|
+
* the browser runs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the account request identity for one balance read.
|
|
18
|
+
*
|
|
19
|
+
* The shipped account bundle inlines `DSH_CLIENT_VERSION` at build time; a plugin
|
|
20
|
+
* bundle has no such define, so the build stamps this package's own version and
|
|
21
|
+
* prefers the shell's value when one is inlined.
|
|
22
|
+
*
|
|
23
|
+
* @param version - the version this build stamps.
|
|
24
|
+
* @returns the metadata the account Remote methods carry.
|
|
25
|
+
*/
|
|
26
|
+
export function clientMetadata(version) {
|
|
27
|
+
const stamped = typeof process !== 'undefined' && process.env !== undefined ? process.env.DSH_CLIENT_VERSION : undefined
|
|
28
|
+
return {
|
|
29
|
+
version: typeof stamped === 'string' && stamped !== '' ? stamped : version,
|
|
30
|
+
locale: typeof navigator !== 'undefined' && typeof navigator.language === 'string' && navigator.language !== ''
|
|
31
|
+
? navigator.language
|
|
32
|
+
: 'en',
|
|
33
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the one-shot balance reader.
|
|
39
|
+
*
|
|
40
|
+
* @param ctx - the browser plugin context; `ctx.get` resolves an optional service.
|
|
41
|
+
* @param version - the version this build stamps into the account metadata.
|
|
42
|
+
* @returns a reader that resolves to the balance view, or undefined when unavailable.
|
|
43
|
+
*/
|
|
44
|
+
export function makeBalanceReader(ctx, version, { timeoutMs = 10000 } = {}) {
|
|
45
|
+
const service = name => {
|
|
46
|
+
try {
|
|
47
|
+
return ctx.get(name)
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return async () => {
|
|
53
|
+
const access = service('remoteAccess')
|
|
54
|
+
const remote = service('remote')
|
|
55
|
+
if (access === undefined || remote === undefined) return undefined
|
|
56
|
+
const metadata = clientMetadata(version)
|
|
57
|
+
return await new Promise(resolve => {
|
|
58
|
+
let settled = false
|
|
59
|
+
let stop
|
|
60
|
+
const timer = setTimeout(() => finish(undefined), timeoutMs)
|
|
61
|
+
const finish = value => {
|
|
62
|
+
if (settled) return
|
|
63
|
+
settled = true
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
resolve(value)
|
|
66
|
+
if (typeof stop === 'function') {
|
|
67
|
+
try {
|
|
68
|
+
stop()
|
|
69
|
+
} catch {
|
|
70
|
+
/* the grant may already have been released */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// The read starts before the yield so an effect runner that stops at the
|
|
75
|
+
// first yielded disposer still performs the call.
|
|
76
|
+
stop = access.whenAccess('account/watch', function* () {
|
|
77
|
+
void (async () => {
|
|
78
|
+
try {
|
|
79
|
+
const result = await remote.account.getBalance(metadata)
|
|
80
|
+
const value = result !== undefined && result.ok === true ? result.value : undefined
|
|
81
|
+
finish(value !== undefined && value.status === 'ready' ? value : undefined)
|
|
82
|
+
} catch {
|
|
83
|
+
finish(undefined)
|
|
84
|
+
}
|
|
85
|
+
})()
|
|
86
|
+
yield () => {
|
|
87
|
+
if (!settled) {
|
|
88
|
+
settled = true
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
resolve(undefined)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
package/client/face.js
CHANGED
|
@@ -313,6 +313,60 @@ function CostPill(props) {
|
|
|
313
313
|
: null)
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
/**
|
|
317
|
+
* The remaining-balance line in the sidebar foot.
|
|
318
|
+
*
|
|
319
|
+
* It renders nothing at all when the switch is off, the account exposes no
|
|
320
|
+
* wallet, or the read was refused — an empty line is better than a line that
|
|
321
|
+
* claims a number the Host did not give us.
|
|
322
|
+
*
|
|
323
|
+
* @param props - the slot's `wide` flag, our lazy `configForms`, and the balance reader.
|
|
324
|
+
* @returns the balance line, or null.
|
|
325
|
+
*/
|
|
326
|
+
function BalanceLine(props) {
|
|
327
|
+
const enabled = useSwitch(props.configForms, 'accountBalance', true)
|
|
328
|
+
const [wallet, setWallet] = React.useState(undefined)
|
|
329
|
+
const read = props.loadBalance
|
|
330
|
+
React.useEffect(() => {
|
|
331
|
+
if (!enabled || typeof read !== 'function') return undefined
|
|
332
|
+
let live = true
|
|
333
|
+
Promise.resolve()
|
|
334
|
+
.then(() => read())
|
|
335
|
+
.then(view => {
|
|
336
|
+
if (live) setWallet(walletOf(view))
|
|
337
|
+
}, () => {
|
|
338
|
+
if (live) setWallet(undefined)
|
|
339
|
+
})
|
|
340
|
+
return () => {
|
|
341
|
+
live = false
|
|
342
|
+
}
|
|
343
|
+
}, [enabled, read])
|
|
344
|
+
if (!enabled) return null
|
|
345
|
+
// The wrapper stays in the DOM even when there is no number to show, so a
|
|
346
|
+
// reader (and the verification script) can tell "registered but empty" from
|
|
347
|
+
// "not registered at all". An empty flex line costs the layout nothing.
|
|
348
|
+
if (wallet === undefined) {
|
|
349
|
+
return React.createElement('div', { 'data-polish-balance': 'empty', style: { display: 'none' } })
|
|
350
|
+
}
|
|
351
|
+
const money = formatBalance(wallet)
|
|
352
|
+
return React.createElement('div', {
|
|
353
|
+
title: 'Remaining account balance',
|
|
354
|
+
style: {
|
|
355
|
+
display: 'flex',
|
|
356
|
+
alignItems: 'center',
|
|
357
|
+
gap: '6px',
|
|
358
|
+
padding: '2px 8px 4px',
|
|
359
|
+
fontSize: '12px',
|
|
360
|
+
lineHeight: '16px',
|
|
361
|
+
minWidth: 0,
|
|
362
|
+
overflow: 'hidden',
|
|
363
|
+
whiteSpace: 'nowrap',
|
|
364
|
+
textOverflow: 'ellipsis',
|
|
365
|
+
opacity: 0.75,
|
|
366
|
+
},
|
|
367
|
+
}, props.wide === false ? money : 'Balance ' + money)
|
|
368
|
+
}
|
|
369
|
+
|
|
316
370
|
return {
|
|
317
371
|
inject: ['slots'],
|
|
318
372
|
/**
|
|
@@ -338,5 +392,12 @@ return {
|
|
|
338
392
|
order: 2,
|
|
339
393
|
inject: () => ({ configForms }),
|
|
340
394
|
}, CostPill))
|
|
395
|
+
const loadBalance = makeBalanceReader(ctx, CLIENT_VERSION)
|
|
396
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
397
|
+
name: 'sidebar.footer.action',
|
|
398
|
+
id: 'polish-balance',
|
|
399
|
+
order: 3,
|
|
400
|
+
inject: () => ({ configForms, loadBalance }),
|
|
401
|
+
}, BalanceLine))
|
|
341
402
|
},
|
|
342
403
|
}
|
package/client/money.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two pure decisions behind the balance line: which wallet to show, and how
|
|
3
|
+
* to render it.
|
|
4
|
+
*
|
|
5
|
+
* They live in their own file because the browser face bundle is only ever run
|
|
6
|
+
* inside the Shell, where nothing can be imported and asserted — so the build
|
|
7
|
+
* inlines this module into `lib/client.js` (with the `export` keywords dropped)
|
|
8
|
+
* while `test/money.spec.mjs` imports it directly. What the test covers is
|
|
9
|
+
* therefore the same source the browser runs, not a copy of it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pick the wallet a balance line should show.
|
|
14
|
+
* @param view - the balance view the account Remote returned.
|
|
15
|
+
* @returns the first wallet carrying a balance, or undefined when the view has none.
|
|
16
|
+
*/
|
|
17
|
+
export function walletOf(view) {
|
|
18
|
+
if (view === undefined || view === null || view.status !== 'ready' || !Array.isArray(view.value)) return undefined
|
|
19
|
+
return view.value.find(wallet => wallet !== null && typeof wallet === 'object' && typeof wallet.balance === 'string')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format one wallet as money.
|
|
24
|
+
* @param wallet - a wallet with `currency` and `balance`.
|
|
25
|
+
* @returns the amount with its currency symbol, at two decimals.
|
|
26
|
+
*/
|
|
27
|
+
export function formatBalance(wallet) {
|
|
28
|
+
const symbol = wallet.currency === 'CNY' ? '\u00A5' : '$'
|
|
29
|
+
const amount = Number(wallet.balance)
|
|
30
|
+
return Number.isFinite(amount) ? symbol + amount.toFixed(2) : symbol + String(wallet.balance)
|
|
31
|
+
}
|
package/client/switches.json
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
"label": "Estimated session cost",
|
|
7
7
|
"description": "Show what the open session has cost so far, priced from recorded usage at the published DeepSeek rates.",
|
|
8
8
|
"defaultOn": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"key": "accountBalance",
|
|
12
|
+
"label": "Account balance",
|
|
13
|
+
"description": "Show the remaining account balance in the sidebar foot, above the account row.",
|
|
14
|
+
"defaultOn": true
|
|
9
15
|
}
|
|
10
16
|
]
|
|
11
17
|
}
|
package/entries/registry.js
CHANGED
|
@@ -40,6 +40,12 @@ export const features = [
|
|
|
40
40
|
defaultOn: true,
|
|
41
41
|
mount: mountCost,
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
id: 'accountBalance',
|
|
45
|
+
label: 'Account balance',
|
|
46
|
+
description: 'Show the remaining account balance in the sidebar foot, above the account row.',
|
|
47
|
+
defaultOn: true,
|
|
48
|
+
},
|
|
43
49
|
]
|
|
44
50
|
|
|
45
51
|
/**
|
package/lib/client.js
CHANGED
|
@@ -13,10 +13,146 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
"label": "Estimated session cost",
|
|
14
14
|
"description": "Show what the open session has cost so far, priced from recorded usage at the published DeepSeek rates.",
|
|
15
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
|
|
16
22
|
}
|
|
17
23
|
]
|
|
18
24
|
const ROW_ID = "polish"
|
|
19
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
|
+
}
|
|
20
156
|
|
|
21
157
|
/**
|
|
22
158
|
* AryaCode Polish — the browser face.
|
|
@@ -333,6 +469,60 @@ window.__ModuleLoader__.load({
|
|
|
333
469
|
: null)
|
|
334
470
|
}
|
|
335
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
|
+
|
|
336
526
|
return {
|
|
337
527
|
inject: ['slots'],
|
|
338
528
|
/**
|
|
@@ -358,6 +548,13 @@ window.__ModuleLoader__.load({
|
|
|
358
548
|
order: 2,
|
|
359
549
|
inject: () => ({ configForms }),
|
|
360
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))
|
|
361
558
|
},
|
|
362
559
|
}
|
|
363
560
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.4",
|
|
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
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -43,8 +43,16 @@
|
|
|
43
43
|
},
|
|
44
44
|
"client": {
|
|
45
45
|
"platform": "web"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"dsh": "^0.2.1-rc.3"
|
|
46
49
|
}
|
|
47
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
|
+
},
|
|
48
56
|
"dependencies": {
|
|
49
57
|
"zod": "^4.4.3"
|
|
50
58
|
},
|
|
@@ -63,9 +71,5 @@
|
|
|
63
71
|
"./package.json": "./package.json",
|
|
64
72
|
"./client": "./lib/client.js",
|
|
65
73
|
"./polish": "./entries/polish.js"
|
|
66
|
-
},
|
|
67
|
-
"scripts": {
|
|
68
|
-
"build:client": "node scripts/build-client.mjs",
|
|
69
|
-
"test": "node --test test/patch.spec.mjs test/config.spec.mjs test/registry.spec.mjs test/cost.spec.mjs test/docs.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs"
|
|
70
74
|
}
|
|
71
|
-
}
|
|
75
|
+
}
|