@aryacode/dsh-aryacode-polish 0.3.7 → 0.4.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 +89 -0
- package/README.md +3 -2
- package/client/account.js +53 -60
- package/client/face.js +354 -75
- package/client/switches.json +6 -0
- package/docs/workspace-folder.md +67 -0
- package/entries/registry.js +6 -0
- package/lib/client.js +414 -136
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -19,11 +19,17 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
"label": "Account balance",
|
|
20
20
|
"description": "Show the remaining account balance in the sidebar foot, above the account row.",
|
|
21
21
|
"defaultOn": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "openWorkspaceFolder",
|
|
25
|
+
"label": "Open workspace folder",
|
|
26
|
+
"description": "Add an “Open workspace folder” row to a Session’s menu that opens its workspace in the file browser.",
|
|
27
|
+
"defaultOn": true
|
|
22
28
|
}
|
|
23
29
|
]
|
|
24
30
|
const ROW_ID = "polish"
|
|
25
31
|
const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
|
|
26
|
-
const CLIENT_VERSION = "0.
|
|
32
|
+
const CLIENT_VERSION = "0.4.4"
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* The two pure decisions behind the balance line: which wallet to show, and how
|
|
@@ -60,12 +66,19 @@ window.__ModuleLoader__.load({
|
|
|
60
66
|
/**
|
|
61
67
|
* Reading the account balance from a plugin face.
|
|
62
68
|
*
|
|
63
|
-
* The account Remote methods are
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
+
* The account Remote methods are the same ones the shipped account row uses. An
|
|
70
|
+
* earlier version waited for the host account bundle's own access grant before
|
|
71
|
+
* reading, on the theory that the grant proves the call is allowed. It does not:
|
|
72
|
+
* that grant belongs to the bundle that owns the account stream, and a plugin
|
|
73
|
+
* that waits for it can wait forever — the sidebar line then stays empty with no
|
|
74
|
+
* error anywhere, which is exactly what a user reports as "I don't see the
|
|
75
|
+
* balance".
|
|
76
|
+
*
|
|
77
|
+
* So this reads directly. The Host still decides: an unauthorised call comes back
|
|
78
|
+
* `{ ok: false }` (or throws) and this resolves to `undefined`. Every failure — no
|
|
79
|
+
* account namespace in this Shell, a refused call, a failed call — resolves to
|
|
80
|
+
* `undefined` instead of throwing, because a sidebar line is never worth breaking
|
|
81
|
+
* the shell over.
|
|
69
82
|
*
|
|
70
83
|
* This module is inlined into `lib/client.js` by `scripts/build-client.mjs` and
|
|
71
84
|
* imported directly by `test/account.spec.mjs`, so the tested source is the one
|
|
@@ -94,63 +107,49 @@ window.__ModuleLoader__.load({
|
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/**
|
|
97
|
-
*
|
|
110
|
+
* The account Remote namespace, or undefined when this Shell has none.
|
|
98
111
|
*
|
|
99
|
-
* @param ctx - the browser plugin context;
|
|
100
|
-
*
|
|
101
|
-
* @returns
|
|
112
|
+
* @param ctx - the browser plugin context; the balance surface is registered
|
|
113
|
+
* inside `ctx.inject(['remote', 'remote.account'], …)` so this normally resolves.
|
|
114
|
+
* @returns the namespace, or undefined.
|
|
102
115
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
function accountService(ctx) {
|
|
117
|
+
try {
|
|
118
|
+
const remote = ctx.remote !== undefined ? ctx.remote : ctx.get('remote')
|
|
119
|
+
const account = remote !== undefined && remote !== null ? remote.account : undefined
|
|
120
|
+
return account !== undefined && account !== null && typeof account.getBalance === 'function' ? account : undefined
|
|
121
|
+
} catch {
|
|
122
|
+
return undefined
|
|
110
123
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read the balance once.
|
|
128
|
+
*
|
|
129
|
+
* @param ctx - the browser plugin context.
|
|
130
|
+
* @param version - the version this build stamps into the account metadata.
|
|
131
|
+
* @param options - `timeoutMs` bounds a call that never settles.
|
|
132
|
+
* @returns the ready balance view, or undefined when there is no number to show.
|
|
133
|
+
*/
|
|
134
|
+
async function readBalance(ctx, version, { timeoutMs = 15000 } = {}) {
|
|
135
|
+
const account = accountService(ctx)
|
|
136
|
+
if (account === undefined) return undefined
|
|
137
|
+
const call = (async () => {
|
|
138
|
+
const result = await account.getBalance(clientMetadata(version))
|
|
139
|
+
if (result === undefined || result === null || result.ok !== true) return undefined
|
|
140
|
+
const value = result.value
|
|
141
|
+
return value !== undefined && value !== null && value.status === 'ready' ? value : undefined
|
|
142
|
+
})().catch(() => undefined)
|
|
143
|
+
let timer
|
|
144
|
+
const timeout = new Promise(resolve => {
|
|
145
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs)
|
|
146
|
+
// A pending balance read must not keep a Node process alive during tests.
|
|
147
|
+
if (typeof timer === 'object' && timer !== null && typeof timer.unref === 'function') timer.unref()
|
|
148
|
+
})
|
|
149
|
+
try {
|
|
150
|
+
return await Promise.race([call, timeout])
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(timer)
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
|
|
@@ -171,6 +170,20 @@ window.__ModuleLoader__.load({
|
|
|
171
170
|
*/
|
|
172
171
|
|
|
173
172
|
const React = require('react')
|
|
173
|
+
const ReactDOM = require('react-dom')
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The Shell's own UI primitives. Guarded because the menu row degrades to a plain
|
|
177
|
+
* button when a Shell answers no primitives, and a bundle that throws on load
|
|
178
|
+
* would take every other surface down with it.
|
|
179
|
+
*/
|
|
180
|
+
const primitives = (() => {
|
|
181
|
+
try {
|
|
182
|
+
return require('@deepseek-ai/dsh-client-ui-primitives')
|
|
183
|
+
} catch {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
})()
|
|
174
187
|
|
|
175
188
|
/** Shared spacing for the page's prose. */
|
|
176
189
|
const SPACING = { margin: '0 0 10px', opacity: 0.85, lineHeight: '1.5' }
|
|
@@ -340,23 +353,131 @@ window.__ModuleLoader__.load({
|
|
|
340
353
|
return Number.isFinite(count) ? new Intl.NumberFormat('en-US').format(count) + ' tok' : ''
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* One label/value line of the breakdown card.
|
|
358
|
+
*
|
|
359
|
+
* Native stat dialogs are a two-column `<dl>`; a flex div per row looks the same
|
|
360
|
+
* until the labels differ in width, so this emits the grid's own children.
|
|
361
|
+
*
|
|
362
|
+
* @param label - the row name.
|
|
363
|
+
* @param value - the reading.
|
|
364
|
+
* @param key - React key.
|
|
365
|
+
* @returns the `dt`/`dd` pair.
|
|
366
|
+
*/
|
|
344
367
|
function detailRow(label, value, key) {
|
|
345
|
-
return React.createElement(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
},
|
|
349
|
-
React.createElement('span', { style: { opacity: 0.75 } }, label),
|
|
350
|
-
React.createElement('span', null, value))
|
|
368
|
+
return React.createElement(React.Fragment, { key },
|
|
369
|
+
React.createElement('dt', null, label),
|
|
370
|
+
React.createElement('dd', null, value))
|
|
351
371
|
}
|
|
352
372
|
|
|
353
373
|
/** The small text under the card that keeps the number honest. */
|
|
354
374
|
const COST_FOOTNOTE = 'Based on recorded usage and published rates. Your provider\'s bill is authoritative.'
|
|
355
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The dock pill's skin, copied from the harness statistics pills
|
|
378
|
+
* (`StatsPills.module.css`) so this one sits in the row instead of looking
|
|
379
|
+
* foreign: same size, same tertiary color, same 14px glyph, never wrapped.
|
|
380
|
+
*/
|
|
381
|
+
const PILL_ANCHOR_STYLE = {
|
|
382
|
+
display: 'inline-flex',
|
|
383
|
+
minWidth: 0,
|
|
384
|
+
fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)',
|
|
385
|
+
lineHeight: 'calc(20px + var(--dsh-content-font-delta-secondary, 0px))',
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The resting pill; hover and open states swap only background and color. */
|
|
389
|
+
const PILL_STYLE = {
|
|
390
|
+
display: 'inline-flex',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
gap: '6px',
|
|
393
|
+
boxSizing: 'border-box',
|
|
394
|
+
maxWidth: '100%',
|
|
395
|
+
padding: '1px 8px',
|
|
396
|
+
border: 'none',
|
|
397
|
+
borderRadius: 'var(--dsw-radius-sm, 4px)',
|
|
398
|
+
background: 'transparent',
|
|
399
|
+
color: 'var(--dsw-alias-label-tertiary, inherit)',
|
|
400
|
+
font: 'inherit',
|
|
401
|
+
fontVariantNumeric: 'tabular-nums',
|
|
402
|
+
lineHeight: 'inherit',
|
|
403
|
+
whiteSpace: 'nowrap',
|
|
404
|
+
cursor: 'pointer',
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The label half of the pill, cut with an ellipsis rather than wrapped. */
|
|
408
|
+
const PILL_LABEL_STYLE = { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
409
|
+
|
|
410
|
+
/** The dialog surface, copied from the harness `stat-dialog.module.css` skin. */
|
|
411
|
+
const PANEL_STYLE = {
|
|
412
|
+
position: 'fixed',
|
|
413
|
+
zIndex: 1100,
|
|
414
|
+
boxSizing: 'border-box',
|
|
415
|
+
width: 'max-content',
|
|
416
|
+
minWidth: 'min(300px, calc(100vw - 24px))',
|
|
417
|
+
maxWidth: 'min(440px, calc(100vw - 24px))',
|
|
418
|
+
padding: '16px',
|
|
419
|
+
border: 0,
|
|
420
|
+
borderRadius: 'var(--dsw-radius-lg, 12px)',
|
|
421
|
+
background: 'var(--dsw-specific-menu, Canvas)',
|
|
422
|
+
backdropFilter: 'var(--dsw-menu-backdrop-filter, none)',
|
|
423
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 10px 30px rgba(0, 0, 0, 0.18))',
|
|
424
|
+
fontSize: '12px',
|
|
425
|
+
lineHeight: '18px',
|
|
426
|
+
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
427
|
+
cursor: 'default',
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Dialog heading row: section name left, headline value right. */
|
|
431
|
+
const PANEL_TITLE_STYLE = {
|
|
432
|
+
display: 'flex',
|
|
433
|
+
justifyContent: 'space-between',
|
|
434
|
+
gap: '16px',
|
|
435
|
+
marginBottom: '8px',
|
|
436
|
+
color: 'var(--dsw-alias-label-primary, inherit)',
|
|
437
|
+
fontWeight: 500,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The rule under a section heading. */
|
|
441
|
+
const PANEL_RULE_STYLE = { marginBottom: '10px', borderTop: '0.5px solid var(--dsw-alias-border-l2, currentColor)' }
|
|
442
|
+
|
|
443
|
+
/** The two-column readings grid. */
|
|
444
|
+
const PANEL_DETAILS_STYLE = {
|
|
445
|
+
display: 'grid',
|
|
446
|
+
gridTemplateColumns: 'minmax(76px, auto) minmax(0, 1fr)',
|
|
447
|
+
gap: '6px 16px',
|
|
448
|
+
margin: 0,
|
|
449
|
+
color: 'var(--dsw-alias-label-tertiary, inherit)',
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A 14px coin glyph, the same weight as the harness pill icons. Drawn inline
|
|
454
|
+
* because a plugin face cannot require the icon package without adding it to the
|
|
455
|
+
* frozen module list.
|
|
456
|
+
* @returns the svg element.
|
|
457
|
+
*/
|
|
458
|
+
function CoinGlyph() {
|
|
459
|
+
return React.createElement('svg', {
|
|
460
|
+
width: 14,
|
|
461
|
+
height: 14,
|
|
462
|
+
viewBox: '0 0 16 16',
|
|
463
|
+
fill: 'none',
|
|
464
|
+
stroke: 'currentColor',
|
|
465
|
+
strokeWidth: 1.3,
|
|
466
|
+
'aria-hidden': true,
|
|
467
|
+
style: { flex: 'none' },
|
|
468
|
+
},
|
|
469
|
+
React.createElement('circle', { cx: 8, cy: 8, r: 6.2 }),
|
|
470
|
+
React.createElement('path', { d: 'M9.6 6.1c-.35-.5-1.85-.86-2.6-.3-.9.66-.35 1.5.75 1.72 1.1.22 2.1.5 1.85 1.5-.3 1.2-2.6 1.3-3.4.5' }),
|
|
471
|
+
React.createElement('path', { d: 'M8 4.4v7.2' }))
|
|
472
|
+
}
|
|
473
|
+
|
|
356
474
|
/**
|
|
357
475
|
* The composer-dock pill, and the breakdown card it opens.
|
|
476
|
+
*
|
|
358
477
|
* Reads the priced projection the Host registers, so no rate table or money
|
|
359
|
-
* arithmetic runs in the browser.
|
|
478
|
+
* arithmetic runs in the browser. The pill is skinned from the harness's own
|
|
479
|
+
* statistics pill so it joins that row instead of standing out of it.
|
|
480
|
+
*
|
|
360
481
|
* @param props - standard session props plus our lazy `configForms`.
|
|
361
482
|
* @returns the pill, or nothing while the feature is off or the session has no usage.
|
|
362
483
|
*/
|
|
@@ -364,22 +485,49 @@ window.__ModuleLoader__.load({
|
|
|
364
485
|
const switched = useSwitch(props.configForms, 'sessionCost', true)
|
|
365
486
|
const usage = typeof props.useProjection === 'function' ? props.useProjection('polishCost') : undefined
|
|
366
487
|
const [open, setOpen] = React.useState(false)
|
|
488
|
+
const [hovered, setHovered] = React.useState(false)
|
|
489
|
+
const [pos, setPos] = React.useState(null)
|
|
367
490
|
const root = React.useRef(null)
|
|
491
|
+
const panel = React.useRef(null)
|
|
492
|
+
|
|
493
|
+
// Anchored like the native dialogs: fixed in the viewport, clamped to a 12px
|
|
494
|
+
// margin so a pill near the window edge cannot push the card off screen.
|
|
495
|
+
React.useEffect(() => {
|
|
496
|
+
if (!open) {
|
|
497
|
+
setPos(null)
|
|
498
|
+
return undefined
|
|
499
|
+
}
|
|
500
|
+
if (root.current === null || typeof window === 'undefined') return undefined
|
|
501
|
+
const rect = root.current.getBoundingClientRect()
|
|
502
|
+
const width = Math.min(440, window.innerWidth - 24)
|
|
503
|
+
setPos({
|
|
504
|
+
left: Math.max(12, Math.min(rect.left, window.innerWidth - 12 - width)),
|
|
505
|
+
bottom: Math.max(12, window.innerHeight - rect.top + 8),
|
|
506
|
+
})
|
|
507
|
+
return undefined
|
|
508
|
+
}, [open])
|
|
368
509
|
|
|
369
510
|
React.useEffect(() => {
|
|
370
511
|
if (!open) return undefined
|
|
371
|
-
const
|
|
372
|
-
|
|
512
|
+
const onPointer = event => {
|
|
513
|
+
const insidePill = root.current !== null && root.current.contains(event.target)
|
|
514
|
+
const insidePanel = panel.current !== null && panel.current.contains(event.target)
|
|
515
|
+
if (!insidePill && !insidePanel) setOpen(false)
|
|
516
|
+
}
|
|
517
|
+
const onKey = event => { if (event.key === 'Escape') setOpen(false) }
|
|
518
|
+
document.addEventListener('pointerdown', onPointer)
|
|
519
|
+
document.addEventListener('keydown', onKey)
|
|
520
|
+
return () => {
|
|
521
|
+
document.removeEventListener('pointerdown', onPointer)
|
|
522
|
+
document.removeEventListener('keydown', onKey)
|
|
373
523
|
}
|
|
374
|
-
document.addEventListener('pointerdown', close)
|
|
375
|
-
return () => document.removeEventListener('pointerdown', close)
|
|
376
524
|
}, [open])
|
|
377
525
|
|
|
378
526
|
if (!switched) return null
|
|
379
527
|
const tokens = typeof usage?.tokens === 'number' ? usage.tokens : 0
|
|
380
528
|
if (tokens === 0) return null
|
|
381
529
|
const money = formatMoney(usage.costUsd)
|
|
382
|
-
const
|
|
530
|
+
const unpriced = usage.incomplete === true || money === ''
|
|
383
531
|
const cacheHit = tokens === 0 ? 0 : Math.round((usage.cacheRead ?? 0) / tokens * 100)
|
|
384
532
|
const models = Array.isArray(usage.models) ? usage.models : []
|
|
385
533
|
|
|
@@ -401,7 +549,7 @@ window.__ModuleLoader__.load({
|
|
|
401
549
|
style: {
|
|
402
550
|
display: 'flex',
|
|
403
551
|
height: '6px',
|
|
404
|
-
margin: '
|
|
552
|
+
margin: '0 0 12px',
|
|
405
553
|
borderRadius: '999px',
|
|
406
554
|
overflow: 'hidden',
|
|
407
555
|
background: 'color-mix(in srgb, CanvasText 10%, transparent)',
|
|
@@ -412,49 +560,27 @@ window.__ModuleLoader__.load({
|
|
|
412
560
|
segment(usage.cacheWrite, 62, 'bar-cache-write'),
|
|
413
561
|
segment(usage.output, 82, 'bar-output'))
|
|
414
562
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
563
|
+
const reading = money === '' ? 'Not priced' : '≈' + money
|
|
564
|
+
const full = reading + (unpriced ? ' — partially priced: some usage has no published rate' : '')
|
|
565
|
+
const pillStyle = { ...PILL_STYLE, ...(hovered || open
|
|
566
|
+
? { background: 'var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,0.12))', color: 'var(--dsw-alias-label-secondary, inherit)' }
|
|
567
|
+
: {}) }
|
|
568
|
+
|
|
569
|
+
const card = React.createElement('div', {
|
|
570
|
+
ref: panel,
|
|
571
|
+
role: 'dialog',
|
|
572
|
+
'aria-label': 'Estimated session cost',
|
|
573
|
+
'data-polish-cost-card': '',
|
|
574
|
+
style: { ...PANEL_STYLE, ...(pos === null ? { left: 12, bottom: 12, visibility: 'hidden' } : pos) },
|
|
418
575
|
},
|
|
419
|
-
React.createElement('
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
style: {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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,
|
|
576
|
+
React.createElement('div', { style: PANEL_TITLE_STYLE },
|
|
577
|
+
React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
|
|
578
|
+
React.createElement(CoinGlyph),
|
|
579
|
+
'Estimated session cost'),
|
|
580
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, money === '' ? 'Not priced' : money)),
|
|
581
|
+
React.createElement('div', { style: PANEL_RULE_STYLE, 'aria-hidden': true }),
|
|
582
|
+
bar,
|
|
583
|
+
React.createElement('dl', { style: PANEL_DETAILS_STYLE },
|
|
458
584
|
detailRow('Cache hit', cacheHit + '%', 'cache-hit'),
|
|
459
585
|
detailRow('Uncached input', formatTokens(usage.uncached), 'uncached'),
|
|
460
586
|
detailRow('Cached input', formatTokens(usage.cacheRead), 'cached'),
|
|
@@ -462,13 +588,39 @@ window.__ModuleLoader__.load({
|
|
|
462
588
|
detailRow('Output', formatTokens(usage.output), 'output'),
|
|
463
589
|
detailRow('Model', modelName, 'model'),
|
|
464
590
|
detailRow('Provider', providerName, 'provider'),
|
|
465
|
-
detailRow('Requests', String(usage.requests ?? 0), 'requests'),
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
: null
|
|
591
|
+
detailRow('Requests', String(usage.requests ?? 0), 'requests')),
|
|
592
|
+
unpriced
|
|
593
|
+
? React.createElement('p', { style: { margin: '10px 0 0', color: 'var(--dsw-alias-label-tertiary, inherit)' } },
|
|
594
|
+
'Partially priced: some usage in this session has no published rate, so this is a lower bound.')
|
|
595
|
+
: null,
|
|
596
|
+
React.createElement('p', { style: { margin: '10px 0 0', color: 'var(--dsw-alias-label-tertiary, inherit)' } }, COST_FOOTNOTE))
|
|
597
|
+
|
|
598
|
+
return React.createElement('span', { ref: root, style: PILL_ANCHOR_STYLE, 'data-composer-stat': 'polish-cost' },
|
|
599
|
+
React.createElement('button', {
|
|
600
|
+
type: 'button',
|
|
601
|
+
className: undefined,
|
|
602
|
+
'aria-haspopup': 'dialog',
|
|
603
|
+
'aria-expanded': open,
|
|
604
|
+
'aria-label': full,
|
|
605
|
+
title: full,
|
|
606
|
+
onClick: () => setOpen(!open),
|
|
607
|
+
onMouseEnter: () => setHovered(true),
|
|
608
|
+
onMouseLeave: () => setHovered(false),
|
|
609
|
+
style: pillStyle,
|
|
610
|
+
},
|
|
611
|
+
React.createElement(CoinGlyph),
|
|
612
|
+
React.createElement('span', { style: PILL_LABEL_STYLE }, reading),
|
|
613
|
+
unpriced ? React.createElement('span', { style: { color: 'var(--dsw-alias-separator-primary, inherit)' } }, '*') : null),
|
|
614
|
+
open && typeof document !== 'undefined' ? ReactDOM.createPortal(card, document.body) : null)
|
|
470
615
|
}
|
|
471
616
|
|
|
617
|
+
/**
|
|
618
|
+
* How long to wait before trying the balance again, in milliseconds. A Shell that
|
|
619
|
+
* has not finished signing in answers the first read with nothing, and a line that
|
|
620
|
+
* gives up after one attempt would stay empty for the life of the page.
|
|
621
|
+
*/
|
|
622
|
+
const BALANCE_RETRY_DELAYS = [1500, 5000, 15000, 30000]
|
|
623
|
+
|
|
472
624
|
/**
|
|
473
625
|
* The remaining-balance line in the sidebar foot.
|
|
474
626
|
*
|
|
@@ -486,15 +638,43 @@ window.__ModuleLoader__.load({
|
|
|
486
638
|
React.useEffect(() => {
|
|
487
639
|
if (!enabled || typeof read !== 'function') return undefined
|
|
488
640
|
let live = true
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
641
|
+
let attempt = 0
|
|
642
|
+
let timer
|
|
643
|
+
const look = () => {
|
|
644
|
+
Promise.resolve()
|
|
645
|
+
.then(() => read())
|
|
646
|
+
.then(view => {
|
|
647
|
+
if (!live) return
|
|
648
|
+
const next = walletOf(view)
|
|
649
|
+
if (next !== undefined) {
|
|
650
|
+
setWallet(next)
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
if (attempt < BALANCE_RETRY_DELAYS.length) {
|
|
654
|
+
timer = setTimeout(look, BALANCE_RETRY_DELAYS[attempt])
|
|
655
|
+
attempt += 1
|
|
656
|
+
}
|
|
657
|
+
}, () => {
|
|
658
|
+
if (!live) return
|
|
659
|
+
if (attempt < BALANCE_RETRY_DELAYS.length) {
|
|
660
|
+
timer = setTimeout(look, BALANCE_RETRY_DELAYS[attempt])
|
|
661
|
+
attempt += 1
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
look()
|
|
666
|
+
// A window that was hidden through sign-in comes back with an answer the
|
|
667
|
+
// first attempt could not have had; retry from the start when it regains focus.
|
|
668
|
+
const onFocus = () => {
|
|
669
|
+
attempt = 0
|
|
670
|
+
clearTimeout(timer)
|
|
671
|
+
look()
|
|
672
|
+
}
|
|
673
|
+
if (typeof window !== 'undefined') window.addEventListener('focus', onFocus)
|
|
496
674
|
return () => {
|
|
497
675
|
live = false
|
|
676
|
+
clearTimeout(timer)
|
|
677
|
+
if (typeof window !== 'undefined') window.removeEventListener('focus', onFocus)
|
|
498
678
|
}
|
|
499
679
|
}, [enabled, read])
|
|
500
680
|
if (!enabled) return null
|
|
@@ -523,6 +703,74 @@ window.__ModuleLoader__.load({
|
|
|
523
703
|
}, props.wide === false ? money : 'Balance ' + money)
|
|
524
704
|
}
|
|
525
705
|
|
|
706
|
+
/**
|
|
707
|
+
* The workspace a Session belongs to.
|
|
708
|
+
*
|
|
709
|
+
* Occupants of `sidebar.workspaces.session.menu.item` address their data by
|
|
710
|
+
* Session id and must not bind or retain the Session, so the workspace is read
|
|
711
|
+
* from the Workspace snapshot at render time and never memoized.
|
|
712
|
+
*
|
|
713
|
+
* @param workspaces - the Shell's Workspace service, when it is loaded.
|
|
714
|
+
* @param sessionId - the row's Session.
|
|
715
|
+
* @returns the owning workspace, or undefined when it cannot be resolved.
|
|
716
|
+
*/
|
|
717
|
+
function workspaceOf(workspaces, sessionId) {
|
|
718
|
+
if (workspaces === undefined || workspaces === null || sessionId === undefined) return undefined
|
|
719
|
+
let items
|
|
720
|
+
try {
|
|
721
|
+
items = workspaces.list.getSnapshot().items
|
|
722
|
+
} catch {
|
|
723
|
+
return undefined
|
|
724
|
+
}
|
|
725
|
+
if (!Array.isArray(items)) return undefined
|
|
726
|
+
return items.find(item => Array.isArray(item.sessionIds)
|
|
727
|
+
&& item.sessionIds.indexOf(sessionId) !== -1
|
|
728
|
+
&& typeof item.path === 'string'
|
|
729
|
+
&& item.path !== '')
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* "Open workspace folder": one row in a Session's "…" menu.
|
|
734
|
+
*
|
|
735
|
+
* It renders nothing when the switch is off or the Session has no workspace the
|
|
736
|
+
* Host could open, because a menu row that cannot act is worse than no row.
|
|
737
|
+
*
|
|
738
|
+
* @param props - the row's Session id, our lazy `configForms`/`workspaces`, the
|
|
739
|
+
* opener, and the slot's injected `useMenuOpenState` hook.
|
|
740
|
+
* @returns the menu row, or null.
|
|
741
|
+
*/
|
|
742
|
+
function OpenFolderMenuItem(props) {
|
|
743
|
+
const enabled = useSwitch(props.configForms, 'openWorkspaceFolder', true)
|
|
744
|
+
// Hooks run unconditionally; the row simply renders nothing when it cannot act.
|
|
745
|
+
const menu = typeof props.useMenuOpenState === 'function' ? props.useMenuOpenState() : undefined
|
|
746
|
+
const closeMenu = Array.isArray(menu) && typeof menu[1] === 'function' ? menu[1] : undefined
|
|
747
|
+
const workspace = enabled ? workspaceOf(props.workspaces, props.sessionId) : undefined
|
|
748
|
+
if (!enabled || workspace === undefined) return null
|
|
749
|
+
const label = 'Open workspace folder'
|
|
750
|
+
const onSelect = () => {
|
|
751
|
+
if (closeMenu !== undefined) closeMenu(false)
|
|
752
|
+
const open = props.openFolder
|
|
753
|
+
if (typeof open !== 'function') return
|
|
754
|
+
void Promise.resolve()
|
|
755
|
+
.then(() => open(workspace))
|
|
756
|
+
.catch(error => {
|
|
757
|
+
// A menu row that silently does nothing is unfixable in the field, so the
|
|
758
|
+
// failure is reported instead of dropped. This is the only diagnostic the
|
|
759
|
+
// bundle writes.
|
|
760
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
761
|
+
console.warn('[aryacode-polish] open workspace folder failed:', error && error.message ? error.message : error)
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
if (primitives !== undefined && typeof primitives.MenuItemButton === 'function') {
|
|
766
|
+
const icon = typeof primitives.IconFolderOpenRegular === 'function'
|
|
767
|
+
? React.createElement(primitives.IconFolderOpenRegular)
|
|
768
|
+
: undefined
|
|
769
|
+
return React.createElement(primitives.MenuItemButton, { icon, onSelect }, label)
|
|
770
|
+
}
|
|
771
|
+
return React.createElement('button', { type: 'button', role: 'menuitem', onClick: onSelect }, label)
|
|
772
|
+
}
|
|
773
|
+
|
|
526
774
|
return {
|
|
527
775
|
inject: ['slots'],
|
|
528
776
|
/**
|
|
@@ -548,13 +796,43 @@ window.__ModuleLoader__.load({
|
|
|
548
796
|
order: 2,
|
|
549
797
|
inject: () => ({ configForms }),
|
|
550
798
|
}, CostPill))
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
799
|
+
// The account Remote namespace is optional: a Shell with no account still gets
|
|
800
|
+
// the Configure page and the cost pill, and only loses the balance line.
|
|
801
|
+
ctx.inject(['remote', 'remote.account'], (accountCtx) => {
|
|
802
|
+
const loadBalance = () => readBalance(accountCtx, CLIENT_VERSION)
|
|
803
|
+
accountCtx.slots.inject('sidebar.footer.action', () => accountCtx.slots.register({
|
|
804
|
+
name: 'sidebar.footer.action',
|
|
805
|
+
id: 'polish-balance',
|
|
806
|
+
order: 3,
|
|
807
|
+
inject: () => ({ configForms, loadBalance }),
|
|
808
|
+
}, BalanceLine))
|
|
809
|
+
})
|
|
810
|
+
// The Workspace service and the Session Remote are both required for the menu
|
|
811
|
+
// row: it reads the folder from the first and opens it through the second.
|
|
812
|
+
// They are declared on the inject list because a Remote namespace may not be
|
|
813
|
+
// read through `ctx.get` — the Shell's guard refuses it.
|
|
814
|
+
ctx.inject(['workspaces', 'remote', 'remote.session'], (workspaceCtx) => {
|
|
815
|
+
const openFolder = async (workspace) => {
|
|
816
|
+
const remote = workspaceCtx.remote.session
|
|
817
|
+
// A Remote method answers an envelope (`{ ok, value }`), not the bare
|
|
818
|
+
// return value; a Shell that answered a plain boolean is tolerated too.
|
|
819
|
+
const answer = await remote.canOpenWorkspacePath()
|
|
820
|
+
const available = answer !== null && typeof answer === 'object'
|
|
821
|
+
? answer.ok === true && answer.value === true
|
|
822
|
+
: answer === true
|
|
823
|
+
if (!available) throw new Error('this Host cannot open a folder')
|
|
824
|
+
const result = await remote.openWorkspacePath({ path: workspace.path })
|
|
825
|
+
if (result !== null && typeof result === 'object' && 'ok' in result && result.ok !== true) {
|
|
826
|
+
throw new Error('the Host refused to open the folder')
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
workspaceCtx.slots.inject('sidebar.workspaces.session.menu.item', () => workspaceCtx.slots.register({
|
|
830
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
831
|
+
id: 'polish-open-folder',
|
|
832
|
+
order: 460,
|
|
833
|
+
inject: () => ({ configForms, openFolder, workspaces: workspaceCtx.get('workspaces') }),
|
|
834
|
+
}, OpenFolderMenuItem))
|
|
835
|
+
})
|
|
558
836
|
},
|
|
559
837
|
}
|
|
560
838
|
},
|