dsh-cline-pass 0.2.3 → 0.2.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/lib/client.js +205 -2
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -241,6 +241,25 @@ window.__ModuleLoader__.load({
|
|
|
241
241
|
'.cp-usage-model-tokens{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary);min-width:52px;text-align:right}',
|
|
242
242
|
'.cp-check{width:14px;height:14px;flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary)}',
|
|
243
243
|
'.cp-tools-label{cursor:pointer;gap:8px}',
|
|
244
|
+
// The usage pill beside the model selector. It borrows the panel's ramp and
|
|
245
|
+
// track treatment so a reading looks the same in both places.
|
|
246
|
+
'.cp-pill{position:relative;display:flex;align-items:center;font-size:12px;line-height:1.4}',
|
|
247
|
+
'.cp-pill-btn{border:0.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:2px 9px;font:inherit;font-size:11.5px;cursor:pointer;white-space:nowrap;font-variant-numeric:tabular-nums}',
|
|
248
|
+
'.cp-pill-btn:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
|
|
249
|
+
'.cp-pill-btn[aria-expanded=true]{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
|
|
250
|
+
'.cp-pill-pop{position:absolute;bottom:calc(100% + 8px);right:0;z-index:40;min-width:236px}',
|
|
251
|
+
'.cp-pill-card{background:var(--dsw-alias-bg-layer-1);border:0.5px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);padding:12px 14px;display:flex;flex-direction:column;gap:9px}',
|
|
252
|
+
'.cp-pill-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
253
|
+
'.cp-pill-row{display:flex;flex-direction:column;gap:4px}',
|
|
254
|
+
'.cp-pill-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--dsw-alias-label-primary)}',
|
|
255
|
+
'.cp-pill-percent{font-variant-numeric:tabular-nums;font-weight:600}',
|
|
256
|
+
'.cp-pill-track{height:4px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);overflow:hidden}',
|
|
257
|
+
'.cp-pill-fill{height:100%;border-radius:999px}',
|
|
258
|
+
'.cp-pill-note,.cp-pill-at{font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
259
|
+
'.cp-pill-at{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:0.5px solid var(--dsw-alias-border-l2);padding-top:8px}',
|
|
260
|
+
'.cp-pill-fail{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
261
|
+
'.cp-pill-retry{border:0;background:0 0;color:var(--dsw-alias-brand-primary);font:inherit;font-size:11.5px;cursor:pointer;padding:0}',
|
|
262
|
+
'.cp-pill-retry:hover{text-decoration:underline}',
|
|
244
263
|
'.cp-model-hidden .cp-mono,.cp-model-hidden .cp-check{opacity:.55}',
|
|
245
264
|
].join('')
|
|
246
265
|
|
|
@@ -306,6 +325,12 @@ window.__ModuleLoader__.load({
|
|
|
306
325
|
usageHours: '{n} 小时',
|
|
307
326
|
usageMinutes: '{n} 分钟',
|
|
308
327
|
usageRefresh: '刷新',
|
|
328
|
+
usagePillTitle: 'Cline Pass 用量',
|
|
329
|
+
usagePillUnavailable: '用量不可用',
|
|
330
|
+
usagePillRetry: '重试',
|
|
331
|
+
usagePillUpdatedAt: '更新于 {time}',
|
|
332
|
+
usagePillLabel: 'Pass',
|
|
333
|
+
usagePillWeekly: '周',
|
|
309
334
|
usageLoading: '正在读取官方额度…',
|
|
310
335
|
usageUnavailable: '暂时读不到官方额度。',
|
|
311
336
|
usageEmpty: '官方未返回额度窗口。',
|
|
@@ -389,6 +414,12 @@ window.__ModuleLoader__.load({
|
|
|
389
414
|
usageHours: '{n}h',
|
|
390
415
|
usageMinutes: '{n}m',
|
|
391
416
|
usageRefresh: 'Refresh',
|
|
417
|
+
usagePillTitle: 'Cline Pass usage',
|
|
418
|
+
usagePillUnavailable: 'Usage unavailable',
|
|
419
|
+
usagePillRetry: 'Retry',
|
|
420
|
+
usagePillUpdatedAt: 'Updated {time}',
|
|
421
|
+
usagePillLabel: 'Pass',
|
|
422
|
+
usagePillWeekly: 'wk',
|
|
392
423
|
usageLoading: 'Reading the official quota…',
|
|
393
424
|
usageUnavailable: 'The official quota is unavailable right now.',
|
|
394
425
|
usageEmpty: 'The gateway reported no quota windows.',
|
|
@@ -1839,10 +1870,164 @@ window.__ModuleLoader__.load({
|
|
|
1839
1870
|
|
|
1840
1871
|
//#endregion
|
|
1841
1872
|
|
|
1873
|
+
//#region usage pill
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* The Cline Pass quota, next to the model selector.
|
|
1877
|
+
*
|
|
1878
|
+
* Only mounted while the selected model belongs to this provider: the
|
|
1879
|
+
* decision the pill informs is "should I use this model right now, and how
|
|
1880
|
+
* much of the window is left", and for any other model the reading is
|
|
1881
|
+
* irrelevant and the request would be wasted. The gate is the whole reason
|
|
1882
|
+
* this is a pill rather than a permanent widget.
|
|
1883
|
+
*/
|
|
1884
|
+
function UsagePill(props) {
|
|
1885
|
+
const state = React.useSyncExternalStore(props.directory.subscribe, props.directory.getSnapshot, props.directory.getSnapshot)
|
|
1886
|
+
const current = state.current
|
|
1887
|
+
if (current === null || current === undefined || String(current.provider) !== String(props.route())) return null
|
|
1888
|
+
return h(UsagePillActive, props)
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** The pill itself: a compact reading, expanding into the window breakdown. */
|
|
1892
|
+
function UsagePillActive(props) {
|
|
1893
|
+
const t = props.t
|
|
1894
|
+
const [snapshot, setSnapshot] = React.useState(null)
|
|
1895
|
+
const [failed, setFailed] = React.useState(false)
|
|
1896
|
+
const [open, setOpen] = React.useState(false)
|
|
1897
|
+
const root = React.useRef(null)
|
|
1898
|
+
// Kept across renders so the timer's callback always sees the latest one.
|
|
1899
|
+
const read = React.useRef(() => {})
|
|
1900
|
+
|
|
1901
|
+
React.useEffect(() => {
|
|
1902
|
+
let alive = true
|
|
1903
|
+
let busy = false
|
|
1904
|
+
/**
|
|
1905
|
+
* One reading, shared by the first paint, the timer and the retry.
|
|
1906
|
+
*
|
|
1907
|
+
* A hidden tab is skipped rather than served: a background poll keeps a
|
|
1908
|
+
* request alive for a number nobody is looking at, and the visibility
|
|
1909
|
+
* handler below refreshes the moment the tab comes back.
|
|
1910
|
+
*/
|
|
1911
|
+
const load = async (manual) => {
|
|
1912
|
+
if (busy) return
|
|
1913
|
+
if (manual !== true && typeof document !== 'undefined' && document.visibilityState === 'hidden') return
|
|
1914
|
+
busy = true
|
|
1915
|
+
try {
|
|
1916
|
+
const value = await props.readUsage()
|
|
1917
|
+
if (alive) { setSnapshot({ value, at: Date.now() }); setFailed(false) }
|
|
1918
|
+
} catch {
|
|
1919
|
+
// The last good reading stays on screen — a failed poll is not worth
|
|
1920
|
+
// blanking a number that was true a minute ago.
|
|
1921
|
+
if (alive) setFailed(true)
|
|
1922
|
+
} finally {
|
|
1923
|
+
busy = false
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
read.current = () => { void load(true) }
|
|
1927
|
+
void load(true)
|
|
1928
|
+
const timer = setInterval(() => { void load(false) }, 60000)
|
|
1929
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(false) }
|
|
1930
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
1931
|
+
return () => {
|
|
1932
|
+
alive = false
|
|
1933
|
+
read.current = () => {}
|
|
1934
|
+
clearInterval(timer)
|
|
1935
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
1936
|
+
}
|
|
1937
|
+
}, [props.readUsage])
|
|
1938
|
+
|
|
1939
|
+
// A click anywhere else closes the popover, which is what a one-shot
|
|
1940
|
+
// disclosure is expected to do; the listener only exists while it is open.
|
|
1941
|
+
React.useEffect(() => {
|
|
1942
|
+
if (!open) return undefined
|
|
1943
|
+
const onDown = (event) => {
|
|
1944
|
+
if (root.current !== null && event.target instanceof Node && !root.current.contains(event.target)) setOpen(false)
|
|
1945
|
+
}
|
|
1946
|
+
document.addEventListener('mousedown', onDown)
|
|
1947
|
+
return () => document.removeEventListener('mousedown', onDown)
|
|
1948
|
+
}, [open])
|
|
1949
|
+
|
|
1950
|
+
const accounts = snapshot?.value?.usage?.accounts ?? []
|
|
1951
|
+
const account = accounts.find((entry) => entry.ok === true) ?? accounts[0]
|
|
1952
|
+
const limits = account?.limits ?? []
|
|
1953
|
+
const byType = new Map(limits.map((limit) => [String(limit.type), limit]))
|
|
1954
|
+
const percentOf = (type) => {
|
|
1955
|
+
const limit = byType.get(type)
|
|
1956
|
+
return limit === undefined ? null : Math.max(0, Math.min(100, Math.round(Number(limit.percentUsed ?? 0))))
|
|
1957
|
+
}
|
|
1958
|
+
const five = percentOf('five_hour')
|
|
1959
|
+
const weekly = percentOf('weekly')
|
|
1960
|
+
|
|
1961
|
+
// The collapsed reading names the two windows a user paces against; a
|
|
1962
|
+
// missing one is dropped rather than shown as an empty slot.
|
|
1963
|
+
const parts = []
|
|
1964
|
+
if (five !== null) parts.push(`${t('usageFiveHour')} ${five}%`)
|
|
1965
|
+
if (weekly !== null) parts.push(`${t('usagePillWeekly')} ${weekly}%`)
|
|
1966
|
+
const summary = [t('usagePillLabel'), ...parts].join(' · ')
|
|
1967
|
+
|
|
1968
|
+
const rows = limits.length === 0 ? null : USAGE_WINDOWS
|
|
1969
|
+
.map(([type, key]) => [type, key, byType.get(type)])
|
|
1970
|
+
.filter(([, , limit]) => limit !== undefined)
|
|
1971
|
+
.concat(limits
|
|
1972
|
+
.filter((limit) => !USAGE_WINDOWS.some(([type]) => type === String(limit.type)))
|
|
1973
|
+
.map((limit) => [String(limit.type), '', limit]))
|
|
1974
|
+
|
|
1975
|
+
const body = failed && snapshot === null
|
|
1976
|
+
? h('div', { key: 'fail', className: 'cp-pill-fail' }, [
|
|
1977
|
+
h('span', { key: 'm' }, t('usagePillUnavailable')),
|
|
1978
|
+
h('button', { key: 'r', type: 'button', className: 'cp-pill-retry', onClick: () => read.current() }, t('usagePillRetry')),
|
|
1979
|
+
])
|
|
1980
|
+
: h('div', { key: 'card', className: 'cp-pill-card' }, [
|
|
1981
|
+
h('div', { key: 'title', className: 'cp-pill-title' }, t('usagePillTitle')),
|
|
1982
|
+
rows === null
|
|
1983
|
+
? h('div', { key: 'empty', className: 'cp-pill-fail' }, t('usagePillUnavailable'))
|
|
1984
|
+
: rows.map(([type, key, limit]) => {
|
|
1985
|
+
const percent = Math.max(0, Math.min(100, Number(limit.percentUsed ?? 0)))
|
|
1986
|
+
const left = countdown(t, limit.resetsAt)
|
|
1987
|
+
return h('div', { key: type, className: 'cp-pill-row' }, [
|
|
1988
|
+
h('div', { key: 'head', className: 'cp-pill-row-head' }, [
|
|
1989
|
+
h('span', { key: 'l' }, key === '' ? type : t(key)),
|
|
1990
|
+
h('span', { key: 'v', className: 'cp-pill-percent' }, `${Math.round(percent)}%`),
|
|
1991
|
+
]),
|
|
1992
|
+
h('div', { key: 'track', className: 'cp-pill-track' },
|
|
1993
|
+
h('div', { key: 'fill', className: 'cp-pill-fill', style: { width: `${percent}%`, background: usageColor(percent) } })),
|
|
1994
|
+
left === '' ? null : h('div', { key: 'note', className: 'cp-pill-note' }, fill(t('usageResetsIn'), { t: left })),
|
|
1995
|
+
])
|
|
1996
|
+
}),
|
|
1997
|
+
snapshot === null ? null : h('div', { key: 'at', className: 'cp-pill-at' }, [
|
|
1998
|
+
h('span', { key: 's' }, fill(t('usagePillUpdatedAt'), { time: stamp(snapshot.at) })),
|
|
1999
|
+
h('button', { key: 'r', type: 'button', className: 'cp-pill-retry', onClick: () => read.current() }, t('usageRefresh')),
|
|
2000
|
+
]),
|
|
2001
|
+
])
|
|
2002
|
+
|
|
2003
|
+
return h('div', { key: 'pill', ref: root, className: 'cp-pill' }, [
|
|
2004
|
+
h('button', {
|
|
2005
|
+
key: 'btn',
|
|
2006
|
+
type: 'button',
|
|
2007
|
+
className: 'cp-pill-btn',
|
|
2008
|
+
'aria-expanded': open,
|
|
2009
|
+
onClick: () => setOpen((was) => !was),
|
|
2010
|
+
}, snapshot === null ? t('usagePillTitle') : summary === '' ? t('usagePillTitle') : summary),
|
|
2011
|
+
open ? h('div', { key: 'pop', className: 'cp-pill-pop' }, body) : null,
|
|
2012
|
+
])
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/** A local timestamp, matching the panel's own reading of "updated at". */
|
|
2016
|
+
function stamp(ms) {
|
|
2017
|
+
const at = new Date(Number(ms ?? 0))
|
|
2018
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
2019
|
+
return `${at.getFullYear()}/${at.getMonth() + 1}/${at.getDate()} ${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}`
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
//#endregion
|
|
2023
|
+
|
|
1842
2024
|
//#region plugin
|
|
1843
2025
|
|
|
1844
|
-
/**
|
|
1845
|
-
|
|
2026
|
+
/**
|
|
2027
|
+
* The slot registry, the browser RPC transport, and the model-selection
|
|
2028
|
+
* directory the usage pill reads the selected provider from.
|
|
2029
|
+
*/
|
|
2030
|
+
const inject = ['slots', 'connection', 'modelDirectories']
|
|
1846
2031
|
|
|
1847
2032
|
/**
|
|
1848
2033
|
* Mount the Cline Pass configuration surface.
|
|
@@ -1903,6 +2088,24 @@ window.__ModuleLoader__.load({
|
|
|
1903
2088
|
inject: face,
|
|
1904
2089
|
}, renderQuick))
|
|
1905
2090
|
|
|
2091
|
+
// 3. The quota reading beside the model selector. It mounts for every
|
|
2092
|
+
// session but renders nothing unless the selected model is this
|
|
2093
|
+
// provider's, so no other model pays for a usage request.
|
|
2094
|
+
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
2095
|
+
name: 'conversation.input.right',
|
|
2096
|
+
id: `${NS}-usage`,
|
|
2097
|
+
order: 100,
|
|
2098
|
+
inject: (sessionId) => ({
|
|
2099
|
+
directory: ctx.modelDirectories.directoryFor(sessionId).store,
|
|
2100
|
+
// The route name is configuration, so it is only known once a state
|
|
2101
|
+
// read has landed. Read it at render time rather than capturing it:
|
|
2102
|
+
// the pill can mount before the first read completes.
|
|
2103
|
+
route: () => controller.store.getSnapshot().data?.provider ?? NS,
|
|
2104
|
+
readUsage: async () => await controller.call('usage', {}),
|
|
2105
|
+
t: translate,
|
|
2106
|
+
}),
|
|
2107
|
+
}, UsagePill))
|
|
2108
|
+
|
|
1906
2109
|
// The first read is lazy (each component asks for it), because the RPC
|
|
1907
2110
|
// transport only exists once Connection has activated.
|
|
1908
2111
|
void controller.refresh()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Self-contained Cline Pass provider for dsh: register Cline Pass subscription models on the harness LLM seam, enumerate and validate the upstream channels behind each model, pin or exclude them with failover, and drive an account pool. No proxy process and no external cline-pass-switcher install required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"platform": "web",
|
|
53
53
|
"inject": [
|
|
54
54
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
55
|
-
"@deepseek-ai/dsh-client-connection"
|
|
55
|
+
"@deepseek-ai/dsh-client-connection",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
56
57
|
]
|
|
57
58
|
},
|
|
58
59
|
"compatibility": {
|