dsh-remote-plugin 0.7.1 → 0.7.3
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/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +322 -19
- package/index.mjs +41 -9
- package/insights.mjs +73 -0
- package/package.json +3 -2
- package/plugin-center.mjs +118 -21
- package/public/admin.html +3 -0
- package/public/admin.js +1 -0
- package/public/app.js +403 -470
- package/public/desktop/desktop.css +101 -0
- package/public/desktop/desktop.html +63 -20
- package/public/desktop/desktop.js +324 -99
- package/public/history.js +160 -0
- package/public/index.html +73 -84
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/link-check.js +109 -0
- package/public/plugin-center.css +100 -1
- package/public/plugin-center.js +222 -60
- package/public/plugin.js +7 -1
- package/public/styles.css +107 -95
- package/public/update.json +12 -12
- package/public/version.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.ins-dialog{--bg:var(--dsr-bg,#f7f8fa);--surface:var(--dsr-panel,#fff);--border:var(--dsr-line,#d9dde5);--text:var(--dsr-text,#1d2635);--muted:var(--dsr-muted,#687386);--accent:var(--dsr-accent,#3979d6);width:min(820px,calc(100vw - 32px));max-height:88dvh;padding:0;border:1px solid var(--border);border-radius:20px;background:var(--bg);color:var(--text);box-shadow:0 24px 90px #0005}
|
|
2
|
+
.ins-dialog::backdrop{background:#10182780;backdrop-filter:blur(5px)}
|
|
3
|
+
.ins-dialog[open]{display:flex;flex-direction:column}.ins-dialog *{box-sizing:border-box}
|
|
4
|
+
.ins-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:24px 24px 16px}.ins-header h2{font-size:21px;line-height:1.4;margin:0}.ins-header p{font-size:12px;color:var(--muted,#687386);margin:5px 0 0;overflow-wrap:anywhere}
|
|
5
|
+
.ins-dialog button{font:inherit;color:inherit;cursor:pointer;border:1px solid var(--border,#d9dde5);background:var(--surface,var(--card,#fff));border-radius:10px;min-height:40px;padding:8px 14px}.ins-header button{font-size:24px;min-width:42px;padding:2px 10px;flex-shrink:0}
|
|
6
|
+
.ins-dialog button:focus-visible{outline:2px solid var(--accent,#3979d6);outline-offset:3px}
|
|
7
|
+
.ins-tabs{display:flex;gap:8px;align-items:center;padding:0 24px 16px;border-bottom:1px solid var(--border,#d9dde5)}.ins-tabs [aria-selected=true]{background:var(--accent,#3979d6);border-color:var(--accent,#3979d6);color:#fff}.ins-tabs [data-ins-refresh]{margin-left:auto;font-size:12px}
|
|
8
|
+
.ins-content{overflow-y:auto;overscroll-behavior:contain;padding:20px 24px;min-height:280px}.ins-section{padding:18px;background:var(--surface,var(--card,#fff));border:1px solid var(--border,#d9dde5);border-radius:14px;margin-bottom:14px}.ins-section h3{font-size:14px;margin:0 0 14px;font-weight:650}.ins-section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.ins-section-head h3{margin:0}.ins-badge{font-size:10px;color:var(--muted,#687386);border:1px solid var(--border,#d9dde5);padding:4px 8px;border-radius:20px;white-space:nowrap}
|
|
9
|
+
.ins-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.ins-metric{display:flex;flex-direction:column;gap:5px;min-width:0}.ins-metric>span{font-size:12px;color:var(--muted,#687386)}.ins-metric strong{font-size:23px;line-height:1.4;font-weight:650;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.ins-metric small,.ins-plan small{font-size:11px;color:var(--muted,#687386);overflow-wrap:anywhere}.ins-note{font-size:12px;line-height:1.7;color:var(--muted,#687386);margin:12px 0 0}.ins-source{font-size:11px;color:var(--muted,#687386);line-height:1.8;margin:16px 0 0}.ins-empty{padding:32px 8px;line-height:1.8;color:var(--muted,#687386);font-size:14px}
|
|
10
|
+
.ins-progress{height:7px;background:var(--border,#e5e9f0);border-radius:9px;overflow:hidden;margin:14px 0 0}.ins-progress>span{display:block;height:100%;background:var(--accent,#3979d6);border-radius:inherit}.ins-category{display:grid;grid-template-columns:85px minmax(0,1fr) 70px;align-items:center;gap:12px;padding:7px 0;font-size:12px}.ins-category strong{text-align:right;font-variant-numeric:tabular-nums;font-weight:500}.ins-track{height:6px;background:var(--border,#e5e9f0);border-radius:5px;overflow:hidden}.ins-track i{display:block;height:100%;background:var(--accent,#3979d6);opacity:.65;border-radius:inherit}
|
|
11
|
+
.ins-events{list-style:none;padding:0;margin:0}.ins-events li{padding:10px 0;border-bottom:1px solid var(--border,#e5e9f0);font-size:12px}.ins-events li:last-child{border:0}.ins-events li>div{display:flex;justify-content:space-between;gap:12px}.ins-events time,.ins-events li>span{font-size:11px;color:var(--muted,#687386)}.ins-events li>span{display:block;margin-top:4px;overflow-wrap:anywhere}.ins-plan{padding:12px 0;border-bottom:1px solid var(--border,#e5e9f0)}.ins-plan:last-child{border:0;padding-bottom:0}.ins-plan>strong{font-size:13px}.ins-plan small{display:block;margin-top:8px}.ins-plan-row{display:flex;justify-content:space-between;gap:12px;font-size:12px;margin-top:14px}.ins-plan .ins-progress{margin-top:7px}
|
|
12
|
+
@media(max-width:560px){.ins-dialog{width:100%;max-width:100%;height:88dvh;max-height:88dvh;margin:auto 0 0;border-radius:20px 20px 0 0;border-bottom:0}.ins-header{padding:20px 18px 14px}.ins-tabs{padding:0 18px 14px;gap:6px}.ins-tabs button{padding:8px 12px;font-size:13px}.ins-content{padding:16px 14px max(20px,env(safe-area-inset-bottom))}.ins-section{padding:16px}.ins-metric strong{font-size:20px}.ins-category{grid-template-columns:76px minmax(0,1fr) 58px;gap:8px}.ins-events li>div{flex-wrap:wrap;gap:4px}.ins-badge{font-size:9px}}
|
|
13
|
+
@media(prefers-reduced-motion:reduce){.ins-dialog::backdrop{backdrop-filter:none}}
|
|
14
|
+
.ins-tabs [aria-selected=true]{color:var(--dsr-on-accent,#fff)}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* Shared read-only context/cost UI. No plugin UI bundles, scripts or settings. */
|
|
2
|
+
;(function (root) {
|
|
3
|
+
'use strict'
|
|
4
|
+
const obj = v => v && typeof v === 'object' && !Array.isArray(v) ? v : null
|
|
5
|
+
const num = v => typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null
|
|
6
|
+
const esc = v => String(v ?? '').slice(0, 240).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
7
|
+
const categories = ['system', 'tools', 'user', 'inject', 'skill', 'assistant', 'tool']
|
|
8
|
+
const labels = {
|
|
9
|
+
zh: { title: '会话洞察', context: '上下文', cost: '费用与额度', close: '关闭', refresh: '刷新', loading: '正在读取…',
|
|
10
|
+
none: '选择一个会话后查看洞察。', missing: '此主机尚未提供 dsh-context 数据。请安装并启用插件后刷新。',
|
|
11
|
+
unavailable: '读取失败,请重试。已有概览仍可查看。', unsupported: '当前插件或 DSH 版本暂不支持此数据格式。',
|
|
12
|
+
costMissing: '此主机尚未提供费用数据。需要启用 dsh-cost-meter,并更新主机端 Remote 插件。',
|
|
13
|
+
occupancy: '上下文占用', modelName: '当前模型', estimated: '组成 Token 为插件估算;占用优先使用 DSH 官方投影。',
|
|
14
|
+
system: '系统提示', tools: '工具定义', user: '用户消息', inject: '注入内容', skill: '技能', assistant: '助手消息', tool: '工具结果',
|
|
15
|
+
composition: '当前组成', events: '最近上下文变化', noEvents: '尚无压缩、裁剪或注入记录。',
|
|
16
|
+
compaction: '压缩', prune: '裁剪', model: '模型变更', mode: '模式变更', counts: '累计变化',
|
|
17
|
+
detailsFailed: '变化记录暂不可用,点击刷新重试。', detailsLoading: '正在读取变化记录…',
|
|
18
|
+
session: '当前会话 API 费用', today: '今日 API 费用', equivalent: '含套餐等值估算', subagents: '子代理 API 费用(单列)',
|
|
19
|
+
balance: '账户余额', plans: '套餐额度 · 已用', source: '数据来源', assembled: '摘要读取于', queried: '供应商查询于',
|
|
20
|
+
estimateNote: '费用由插件账本按用量估算,以供应商账单为准;套餐等值金额不计入 API 费用,子代理费用单独列出。',
|
|
21
|
+
sessionMissing: '插件账本尚无此会话记录', noPlans: '主机未启用可展示的套餐额度。', off: '主机未开启查询',
|
|
22
|
+
error: '供应商查询失败', stale: '缓存数据', unknown: '暂无有效数据', unlimited: '不限量', reset: '重置',
|
|
23
|
+
rolling: '滚动窗口', weekly: '每周', monthly: '每月', date: '主机记账日', details: '明细' },
|
|
24
|
+
en: { title: 'Session insights', context: 'Context', cost: 'Cost & quotas', close: 'Close', refresh: 'Refresh', loading: 'Loading…',
|
|
25
|
+
none: 'Select a session to view insights.', missing: 'This host has no dsh-context data. Enable the plugin, then refresh.',
|
|
26
|
+
unavailable: 'Could not read data. Retry; the current overview is still available.', unsupported: 'This plugin or DSH version uses an unsupported data format.',
|
|
27
|
+
costMissing: 'Enable dsh-cost-meter and update the host Remote plugin to read costs.', occupancy: 'Context occupancy', modelName: 'Current model',
|
|
28
|
+
estimated: 'Composition tokens are plugin estimates; occupancy uses the DSH projection when available.',
|
|
29
|
+
system: 'System prompt', tools: 'Tool definitions', user: 'User messages', inject: 'Injections', skill: 'Skills', assistant: 'Assistant', tool: 'Tool results',
|
|
30
|
+
composition: 'Current composition', events: 'Recent context changes', noEvents: 'No compaction, pruning or injection records yet.',
|
|
31
|
+
compaction: 'Compaction', prune: 'Pruning', model: 'Model change', mode: 'Mode change', counts: 'Total changes',
|
|
32
|
+
detailsFailed: 'Change records are unavailable. Refresh to retry.', detailsLoading: 'Loading change records…',
|
|
33
|
+
session: 'Session API cost', today: 'Today API cost', equivalent: 'Estimate including plan equivalents', subagents: 'Subagent API cost (separate)',
|
|
34
|
+
balance: 'Account balance', plans: 'Plan quotas · used', source: 'Source', assembled: 'Summary read at', queried: 'Provider queried at',
|
|
35
|
+
estimateNote: 'Costs are usage estimates from the plugin ledger. Provider invoices are authoritative. Plan equivalents and subagents are shown separately.',
|
|
36
|
+
sessionMissing: 'No ledger record for this session yet', noPlans: 'No visible plan quota is enabled on this host.', off: 'Query disabled on host',
|
|
37
|
+
error: 'Provider query failed', stale: 'Cached data', unknown: 'No valid data', unlimited: 'Unlimited', reset: 'Resets',
|
|
38
|
+
rolling: 'Rolling window', weekly: 'Weekly', monthly: 'Monthly', date: 'Host ledger date', details: 'Details' },
|
|
39
|
+
}
|
|
40
|
+
let lang = 'zh'
|
|
41
|
+
const t = key => labels[lang][key] || key
|
|
42
|
+
const tokens = v => num(v) === null ? '—' : new Intl.NumberFormat(lang === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 0 }).format(v)
|
|
43
|
+
const money = (v, currency = 'USD', digits = 4) => num(v) === null || !/^[A-Z]{3}$/.test(currency) ? '—' : currency + ' ' + v.toFixed(digits)
|
|
44
|
+
const time = v => typeof v === 'number' && v > 0 && v < 8640000000000000 ? new Date(v).toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US') : '—'
|
|
45
|
+
function contextOf(value) {
|
|
46
|
+
const data = obj(value), current = obj(data?.current)
|
|
47
|
+
if (!data || !current || num(current.total) === null) return null
|
|
48
|
+
if (categories.some(key => num(current[key]) === null)) return null
|
|
49
|
+
return { ...data, current: Object.fromEntries([...categories, 'total'].map(key => [key, current[key]])) }
|
|
50
|
+
}
|
|
51
|
+
function costOf(value) {
|
|
52
|
+
const data = obj(value)
|
|
53
|
+
if (data?.schema !== 1 || data.available !== true || data.source !== 'dsh-cost-meter' || data.currency !== 'USD') return null
|
|
54
|
+
return data
|
|
55
|
+
}
|
|
56
|
+
function metric(label, value, note = '') {
|
|
57
|
+
return `<div class="ins-metric"><span>${esc(label)}</span><strong>${esc(value)}</strong>${note ? `<small>${esc(note)}</small>` : ''}</div>`
|
|
58
|
+
}
|
|
59
|
+
function bar(percent, label) {
|
|
60
|
+
const value = num(percent)
|
|
61
|
+
return value === null ? '' : `<div class="ins-progress" role="progressbar" aria-label="${esc(label)}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.min(value, 100)}"><span style="width:${Math.min(value, 100)}%"></span></div>`
|
|
62
|
+
}
|
|
63
|
+
function contextHtml(session, detail, detailStatus) {
|
|
64
|
+
const values = obj(session?.projections?.values) || {}, head = contextOf(values.contextTimeline)
|
|
65
|
+
// Only combine a detail with the projection revision that requested it.
|
|
66
|
+
const compatible = detail && (!head || head.detailRev === detail.rev)
|
|
67
|
+
const timeline = head || (compatible ? contextOf(detail.head) : null)
|
|
68
|
+
if (timeline?.unsupported) return `<p class="ins-empty">${esc(t('unsupported'))}</p>`
|
|
69
|
+
if (!timeline) return `<p class="ins-empty">${esc(detailStatus === 'loading' ? t('loading') : detailStatus === 'error' ? t('unavailable') : t('missing'))}</p>`
|
|
70
|
+
const pressure = obj(values.contextPressure), occupied = num(pressure?.pressureTokens) ?? timeline.current.total
|
|
71
|
+
const window = num(pressure?.contextWindow) ?? num(timeline.contextWindow), percent = window > 0 ? occupied / window * 100 : null
|
|
72
|
+
let html = `<section class="ins-section"><div class="ins-section-head"><h3>${esc(t('occupancy'))}</h3><span class="ins-badge">dsh-context</span></div>
|
|
73
|
+
<div class="ins-metrics">${metric(t('occupancy'), tokens(occupied), window > 0 ? '/ ' + tokens(window) + ' tokens' : '')}${metric(t('modelName'), timeline.model || '—', timeline.provider || '')}</div>
|
|
74
|
+
${bar(percent, t('occupancy'))}<p class="ins-note">${esc(t('estimated'))}</p></section>
|
|
75
|
+
<section class="ins-section"><h3>${esc(t('composition'))}</h3><div class="ins-composition">`
|
|
76
|
+
for (const key of categories) {
|
|
77
|
+
const count = timeline.current[key], share = timeline.current.total > 0 ? count / timeline.current.total * 100 : 0
|
|
78
|
+
html += `<div class="ins-category"><span>${esc(t(key))}</span><span class="ins-track"><i style="width:${Math.min(share, 100)}%"></i></span><strong>${tokens(count)}</strong></div>`
|
|
79
|
+
}
|
|
80
|
+
html += '</div></section>'
|
|
81
|
+
const counts = obj(timeline.counts)
|
|
82
|
+
if (counts) html += `<section class="ins-section"><h3>${esc(t('counts'))}</h3><div class="ins-metrics">${['compactions', 'prunes', 'injects'].map((key, i) => metric(t(['compaction', 'prune', 'inject'][i]), tokens(counts[key]))).join('')}</div></section>`
|
|
83
|
+
const events = head?.detailRev !== undefined ? (compatible ? detail.events : []) : timeline.events
|
|
84
|
+
html += `<section class="ins-section"><h3>${esc(t('events'))}</h3>`
|
|
85
|
+
if (head?.detailRev !== undefined && !compatible) html += `<p class="ins-note">${esc(t(detailStatus === 'loading' ? 'detailsLoading' : 'detailsFailed'))}</p>`
|
|
86
|
+
else if (!Array.isArray(events) || !events.length) html += `<p class="ins-note">${esc(t('noEvents'))}</p>`
|
|
87
|
+
else html += '<ol class="ins-events">' + events.filter(row => obj(row) && ['compaction', 'prune', 'inject', 'model', 'mode'].includes(row.kind)).slice(-8).reverse().map(row => `<li><div><strong>${esc(t(row.kind))}</strong><time>${esc(time(row.time))}</time></div><span>${esc(row.name || row.sub || '')}${num(row.tokens) !== null ? ' · ' + tokens(row.tokens) + ' tokens' : ''}</span></li>`).join('') + '</ol>'
|
|
88
|
+
return html + `</section><p class="ins-source">${esc(t('source'))} · dsh-context / DSH</p>`
|
|
89
|
+
}
|
|
90
|
+
function costHtml(raw, phase) {
|
|
91
|
+
const data = costOf(raw)
|
|
92
|
+
if (!data) return `<p class="ins-empty">${esc(t(phase === 'loading' ? 'loading' : phase === 'error' ? 'unavailable' : raw?.available === false ? 'costMissing' : 'unsupported'))}</p>`
|
|
93
|
+
const own = data.session?.found === true ? data.session.own : null
|
|
94
|
+
let html = (phase === 'error' ? `<p role="status" class="ins-note">${esc(t('unavailable'))}</p>` : '') + `<section class="ins-section"><div class="ins-section-head"><h3>${esc(t('cost'))}</h3><span class="ins-badge">dsh-cost-meter</span></div><div class="ins-metrics">
|
|
95
|
+
${metric(t('session'), money(own?.apiCost), own ? '' : t('sessionMissing'))}${metric(t('today'), money(data.today?.apiCost), data.day ? t('date') + ' · ' + data.day : '')}
|
|
96
|
+
${own && num(own.cost) !== null && own.cost !== own.apiCost ? metric(t('equivalent'), money(own.cost)) : ''}
|
|
97
|
+
${num(data.session?.subagentCount) > 0 ? metric(t('subagents'), money(data.session?.subagents?.apiCost)) : ''}</div><p class="ins-note">${esc(t('estimateNote'))}</p></section>`
|
|
98
|
+
if (obj(data.balance)) {
|
|
99
|
+
const balance = data.balance
|
|
100
|
+
html += `<section class="ins-section"><h3>${esc(t('balance'))}</h3><div class="ins-metrics">${metric(t('balance'), balance.status === 'ok' ? money(balance.total, balance.currency, 2) : '—', balance.status === 'ok' ? t('queried') + ' · ' + time(balance.fetchedAt) : t(['off', 'error', 'stale'].includes(balance.status) ? balance.status : 'unknown'))}</div></section>`
|
|
101
|
+
}
|
|
102
|
+
html += `<section class="ins-section"><h3>${esc(t('plans'))}</h3>`
|
|
103
|
+
const plans = Array.isArray(data.plans) ? data.plans.slice(0, 20) : []
|
|
104
|
+
if (!plans.length) html += `<p class="ins-note">${esc(t('noPlans'))}</p>`
|
|
105
|
+
for (const rawPlan of plans) {
|
|
106
|
+
const plan = obj(rawPlan)
|
|
107
|
+
if (!plan) continue
|
|
108
|
+
html += `<div class="ins-plan"><strong>${esc(plan.name)}</strong>`
|
|
109
|
+
if (plan.status !== 'ok') html += `<p class="ins-note">${esc(t(['off', 'error', 'stale'].includes(plan.status) ? plan.status : 'unknown'))}</p>`
|
|
110
|
+
else {
|
|
111
|
+
const rows = Array.isArray(plan.windows) ? plan.windows.slice(0, 8) : []
|
|
112
|
+
if (!rows.length) html += `<p class="ins-note">${esc(t('unknown'))}</p>`
|
|
113
|
+
for (const rawRow of rows) {
|
|
114
|
+
const row = obj(rawRow)
|
|
115
|
+
if (!row) continue
|
|
116
|
+
const percent = num(row.percent)
|
|
117
|
+
html += `<div class="ins-plan-row"><span>${esc(t(row.name))}</span><strong>${row.unlimited === true ? esc(t('unlimited')) : percent !== null && percent <= 100 ? percent.toFixed(1) + '%' : '—'}</strong></div>${row.unlimited === true ? '' : bar(percent, plan.name + ' ' + row.name)}${row.resetsAt ? `<small>${esc(t('reset'))} · ${esc(row.resetsAt)}</small>` : ''}`
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
html += `<small>${esc(t('queried'))} · ${esc(time(plan.fetchedAt))}</small></div>`
|
|
121
|
+
}
|
|
122
|
+
return html + `</section><p class="ins-source">${esc(t('source'))} · dsh-cost-meter<br>${esc(t('assembled'))} · ${esc(time(data.generatedAt))}</p>`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let dialog, options, tab = 'context', generation = 0, controller, poll, debounce
|
|
126
|
+
let detail = null, detailStatus = '', cost = null, costStatus = '', detailTask = false, detailRevision
|
|
127
|
+
function current() { return options?.getSession?.() }
|
|
128
|
+
function valid(g) { return g === generation && dialog?.open && options.connection.valid() && current()?.sessionId === options.sessionId }
|
|
129
|
+
function render() {
|
|
130
|
+
if (!dialog?.open) return
|
|
131
|
+
if (!valid(generation)) { close(); return }
|
|
132
|
+
dialog.querySelector('.ins-content').innerHTML = tab === 'context' ? contextHtml(current(), detail, detailStatus) : costHtml(cost, costStatus)
|
|
133
|
+
for (const button of dialog.querySelectorAll('[data-ins-tab]')) {
|
|
134
|
+
const active = button.dataset.insTab === tab
|
|
135
|
+
button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1
|
|
136
|
+
}
|
|
137
|
+
dialog.querySelector('.ins-content').setAttribute('aria-labelledby', 'ins-tab-' + tab)
|
|
138
|
+
}
|
|
139
|
+
async function request(path, body, g) {
|
|
140
|
+
if (!valid(g)) throw new Error('stale')
|
|
141
|
+
const connection = options.connection
|
|
142
|
+
const parent = controller.signal, attempt = new AbortController()
|
|
143
|
+
const abort = () => attempt.abort()
|
|
144
|
+
parent.addEventListener('abort', abort, { once: true })
|
|
145
|
+
const deadline = setTimeout(abort, 25000)
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(options.url(path), { method: body ? 'POST' : 'GET', headers: { authorization: 'Bearer ' + connection.token,
|
|
148
|
+
'x-dsh-remote-client': 'web', 'content-type': 'application/json' }, ...(body ? { body: JSON.stringify(body) } : {}), signal: attempt.signal })
|
|
149
|
+
if (!valid(g)) throw new Error('stale')
|
|
150
|
+
if (!response.ok) { const error = new Error('read-failed'); error.status = response.status; throw error }
|
|
151
|
+
const source = await response.text()
|
|
152
|
+
if (!valid(g)) throw new Error('stale')
|
|
153
|
+
if (source.length > 2 * 1024 * 1024) throw new Error('too-large')
|
|
154
|
+
const data = JSON.parse(source)
|
|
155
|
+
if (!obj(data) || data.ok !== true) throw new Error('invalid-response')
|
|
156
|
+
return data
|
|
157
|
+
} finally { clearTimeout(deadline); parent.removeEventListener('abort', abort) }
|
|
158
|
+
}
|
|
159
|
+
async function loadContext(force = false) {
|
|
160
|
+
const g = generation, head = contextOf(current()?.projections?.values?.contextTimeline)
|
|
161
|
+
if (!valid(g) || tab !== 'context' || detailTask || head?.unsupported) return
|
|
162
|
+
if (head && head.detailRev === undefined) { detailStatus = ''; render(); return }
|
|
163
|
+
const revision = head?.detailRev ?? null
|
|
164
|
+
if (!force && detail && detailRevision === revision) return
|
|
165
|
+
detailTask = true; detailStatus = 'loading'; render()
|
|
166
|
+
try {
|
|
167
|
+
const data = await request('/api/dsh-context/detail', { sessionId: options.sessionId }, g)
|
|
168
|
+
if (!valid(g)) return
|
|
169
|
+
const next = obj(data.value), newest = contextOf(current()?.projections?.values?.contextTimeline)
|
|
170
|
+
if (next && num(next.rev) !== null && Array.isArray(next.events) && contextOf(next.head)) {
|
|
171
|
+
if (newest?.detailRev !== undefined && newest.detailRev !== next.rev) { detailStatus = 'changed'; return }
|
|
172
|
+
detail = { rev: next.rev, head: next.head, events: next.events }; detailRevision = newest?.detailRev ?? null; detailStatus = ''
|
|
173
|
+
} else if (next === null) detailStatus = 'missing'
|
|
174
|
+
else detailStatus = 'error'
|
|
175
|
+
} catch (error) { if (valid(g)) detailStatus = error.status === 404 ? 'missing' : 'error' }
|
|
176
|
+
finally {
|
|
177
|
+
if (g === generation) { detailTask = false; render(); if (detailStatus === 'changed') scheduleContext() }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function loadCost() {
|
|
181
|
+
const g = generation
|
|
182
|
+
if (!valid(g) || tab !== 'cost' || costStatus === 'loading') return
|
|
183
|
+
costStatus = 'loading'; render()
|
|
184
|
+
try {
|
|
185
|
+
const data = await request('/remote/api/insights/cost?sessionId=' + encodeURIComponent(options.sessionId), null, g)
|
|
186
|
+
if (!valid(g)) return
|
|
187
|
+
if (data.available === false || costOf(data)) { cost = data; costStatus = '' }
|
|
188
|
+
else costStatus = 'error'
|
|
189
|
+
} catch (error) { if (valid(g)) { costStatus = 'error'; if (error.status === 404) { cost = { available: false }; costStatus = '' } } }
|
|
190
|
+
finally { if (valid(g)) render() }
|
|
191
|
+
}
|
|
192
|
+
function scheduleContext() { clearTimeout(debounce); debounce = setTimeout(() => loadContext(), 400) }
|
|
193
|
+
function close() {
|
|
194
|
+
generation++; controller?.abort(); clearInterval(poll); clearTimeout(debounce)
|
|
195
|
+
if (dialog?.open) dialog.close()
|
|
196
|
+
options = null; detail = null; cost = null; detailTask = false; detailStatus = ''; costStatus = ''
|
|
197
|
+
}
|
|
198
|
+
function open(opts) {
|
|
199
|
+
close(); options = opts; tab = opts.tab === 'cost' ? 'cost' : 'context'; lang = root.I18N?.lang === 'en' ? 'en' : 'zh'
|
|
200
|
+
options.sessionId = current()?.sessionId
|
|
201
|
+
if (!options.sessionId) return
|
|
202
|
+
controller = new AbortController(); detailRevision = undefined
|
|
203
|
+
if (!dialog) {
|
|
204
|
+
dialog = document.createElement('dialog'); dialog.className = 'ins-dialog'; document.body.append(dialog)
|
|
205
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); close() })
|
|
206
|
+
dialog.addEventListener('close', () => { if (options && !dialog.open) close() })
|
|
207
|
+
}
|
|
208
|
+
dialog.innerHTML = `<header class="ins-header"><div><h2 id="ins-title">${esc(t('title'))}</h2><p>${esc(opts.getTitle?.() || options.sessionId)}</p></div><button type="button" data-ins-close aria-label="${esc(t('close'))}">×</button></header>
|
|
209
|
+
<nav class="ins-tabs" role="tablist" aria-label="${esc(t('title'))}"><button type="button" role="tab" id="ins-tab-context" data-ins-tab="context" aria-controls="ins-content">${esc(t('context'))}</button><button type="button" role="tab" id="ins-tab-cost" data-ins-tab="cost" aria-controls="ins-content">${esc(t('cost'))}</button><button type="button" data-ins-refresh>${esc(t('refresh'))}</button></nav><div id="ins-content" class="ins-content" role="tabpanel" tabindex="0"></div>`
|
|
210
|
+
dialog.setAttribute('aria-labelledby', 'ins-title')
|
|
211
|
+
dialog.querySelector('[data-ins-close]').addEventListener('click', close)
|
|
212
|
+
function select(value) { tab = value; render(); tab === 'cost' ? loadCost() : loadContext() }
|
|
213
|
+
dialog.querySelectorAll('[data-ins-tab]').forEach(button => {
|
|
214
|
+
button.addEventListener('click', () => select(button.dataset.insTab))
|
|
215
|
+
button.addEventListener('keydown', event => {
|
|
216
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
|
|
217
|
+
event.preventDefault(); select(event.key === 'Home' ? 'context' : event.key === 'End' ? 'cost' : tab === 'context' ? 'cost' : 'context')
|
|
218
|
+
dialog.querySelector('[data-ins-tab="' + tab + '"]').focus()
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
dialog.querySelector('[data-ins-refresh]').addEventListener('click', () => tab === 'cost' ? loadCost() : loadContext(true))
|
|
222
|
+
dialog.showModal(); render(); tab === 'cost' ? loadCost() : loadContext()
|
|
223
|
+
poll = setInterval(() => { if (!valid(generation)) close(); else if (tab === 'cost') loadCost(); else { render(); loadContext() } }, 60000)
|
|
224
|
+
}
|
|
225
|
+
const api = { open, close, update() { if (dialog?.open) { render(); if (dialog.open && tab === 'context') scheduleContext() } }, contextOf, costOf, contextHtml, costHtml }
|
|
226
|
+
root.DshInsights = api
|
|
227
|
+
if (typeof module !== 'undefined') module.exports = api
|
|
228
|
+
})(typeof window !== 'undefined' ? window : globalThis)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Active overview checks. Reuses the existing WS reconnect policy; no extra heartbeat frames. */
|
|
2
|
+
'use strict';
|
|
3
|
+
;(function(root){
|
|
4
|
+
class LinkCheck {
|
|
5
|
+
constructor(options){
|
|
6
|
+
this.options=options;this.timer=null;this.graceTimer=null;this.epoch=0;this.job=null;this.abort=null;this.attempt=0
|
|
7
|
+
this.model={phase:'idle',gateway:null,probing:false,checks:{gateway:false,dsh:false,mux:false,host:false}}
|
|
8
|
+
this.now=options.now||Date.now;this.setTimer=options.setTimer||((fn,delay)=>setTimeout(fn,delay));this.clearTimer=options.clearTimer||(id=>clearTimeout(id))
|
|
9
|
+
}
|
|
10
|
+
emit(){this.options.changed?.(this.model)}
|
|
11
|
+
clearTimers(){this.clearTimer(this.timer);this.clearTimer(this.graceTimer);this.timer=this.graceTimer=null}
|
|
12
|
+
pause(){this.epoch++;this.clearTimers();this.abort?.abort();this.abort=null;this.job=null;this.model.probing=false;this.model.phase='idle'}
|
|
13
|
+
start(force=false){
|
|
14
|
+
if(!this.options.active()){this.pause();return}
|
|
15
|
+
if(!force&&this.model.phase!=='idle'){this.observe();return}
|
|
16
|
+
if(force&&this.model.probing&&this.options.configured()&&this.options.online())return
|
|
17
|
+
this.pause();this.attempt=0;this.deadline=this.now()+20000;this.model.gateway=null
|
|
18
|
+
this.model.phase=!this.options.configured()?'unconfigured':!this.options.online()?'offline':'checking'
|
|
19
|
+
this.model.checks=this.options.snapshot();this.emit()
|
|
20
|
+
if(this.model.phase!=='checking')return
|
|
21
|
+
const epoch=this.epoch
|
|
22
|
+
this.graceTimer=this.setTimer(()=>{if(epoch!==this.epoch)return;this.graceTimer=null;if(this.model.phase==='checking'){this.model.phase='degraded';this.emit()}},20000)
|
|
23
|
+
void this.run()
|
|
24
|
+
}
|
|
25
|
+
observe(){
|
|
26
|
+
if(!this.options.active())return
|
|
27
|
+
const checks=this.options.snapshot(),key=JSON.stringify(checks),previous=JSON.stringify(this.model.checks)
|
|
28
|
+
this.model.checks=checks
|
|
29
|
+
if(this.options.configured()&&this.options.online()&&Object.values(checks).every(Boolean)){
|
|
30
|
+
if(this.model.phase!=='ready'){this.model.phase='ready';this.clearTimer(this.graceTimer);this.graceTimer=null;if(!this.model.probing)this.schedule(30000);this.emit()}
|
|
31
|
+
else if(key!==previous)this.emit()
|
|
32
|
+
}else if(this.model.phase==='ready'){
|
|
33
|
+
this.start(true)
|
|
34
|
+
}else if(key!==previous)this.emit()
|
|
35
|
+
}
|
|
36
|
+
schedule(delay){this.clearTimer(this.timer);const epoch=this.epoch;this.timer=this.setTimer(()=>{this.timer=null;if(epoch===this.epoch)void this.run()},delay)}
|
|
37
|
+
async run(){
|
|
38
|
+
if(!this.options.active()){this.pause();return}
|
|
39
|
+
if(!this.options.configured()||!this.options.online()){this.start(true);return}
|
|
40
|
+
if(this.job)return this.job
|
|
41
|
+
const epoch=this.epoch,abort=new AbortController();this.abort=abort;this.model.probing=true;this.emit()
|
|
42
|
+
const job=Promise.resolve().then(async()=>{
|
|
43
|
+
try{
|
|
44
|
+
this.options.repair()
|
|
45
|
+
const result=await this.options.probe(abort.signal)
|
|
46
|
+
if(epoch!==this.epoch)return
|
|
47
|
+
this.model.gateway=result.gateway===true
|
|
48
|
+
}catch{if(epoch===this.epoch)this.model.gateway=false}
|
|
49
|
+
if(epoch!==this.epoch)return
|
|
50
|
+
this.model.checks=this.options.snapshot()
|
|
51
|
+
const complete=Object.values(this.model.checks).every(Boolean)
|
|
52
|
+
this.model.phase=complete?'ready':this.now()<this.deadline?'checking':'degraded'
|
|
53
|
+
this.model.probing=false;this.emit()
|
|
54
|
+
if(complete){this.clearTimer(this.graceTimer);this.graceTimer=null}
|
|
55
|
+
if(this.options.active())this.schedule(complete||this.model.phase==='degraded'?30000:[2000,5000,10000][Math.min(this.attempt++,2)])
|
|
56
|
+
}).finally(()=>{if(epoch===this.epoch&&this.job===job){this.job=null;this.abort=null}})
|
|
57
|
+
this.job=job;return job
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/* Visual feedback never delays or changes network state. CSS owns the continuous sweep. */
|
|
61
|
+
class PulseVisual {
|
|
62
|
+
constructor(options){
|
|
63
|
+
this.card=options.card;this.active=options.active||(()=>!root.document?.hidden)
|
|
64
|
+
this.reduced=options.reduced||(()=>!!root.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
|
|
65
|
+
this.setTimer=options.setTimer||((fn,delay)=>setTimeout(fn,delay));this.clearTimer=options.clearTimer||(id=>clearTimeout(id))
|
|
66
|
+
this.checking=false;this.settleTimer=null;this.textJobs=new Map()
|
|
67
|
+
root.matchMedia?.('(prefers-reduced-motion: reduce)').addEventListener?.('change',event=>{if(event.matches)this.pause();else this.setChecking(this.card.classList.contains('status-checking'))})
|
|
68
|
+
}
|
|
69
|
+
setChecking(checking){
|
|
70
|
+
if(!this.active()||this.reduced()){this.pause();return}
|
|
71
|
+
if(checking===this.checking)return
|
|
72
|
+
this.checking=checking;this.clearTimer(this.settleTimer);this.settleTimer=null
|
|
73
|
+
if(checking)this.card.dataset.pulseRunning='true'
|
|
74
|
+
else this.settleTimer=this.setTimer(()=>{this.settleTimer=null;this.card.dataset.pulseRunning='false'},240)
|
|
75
|
+
}
|
|
76
|
+
setLinks(checks){
|
|
77
|
+
for(const segment of this.card.querySelectorAll('[data-pulse-link]'))segment.setAttribute('data-confirmed',String(checks[segment.getAttribute('data-pulse-link')]===true))
|
|
78
|
+
}
|
|
79
|
+
clearText(node){
|
|
80
|
+
const previous=this.textJobs.get(node);if(!previous)return
|
|
81
|
+
this.textJobs.delete(node);previous.enter?.cancel();previous.exit?.cancel();previous.echo?.remove()
|
|
82
|
+
}
|
|
83
|
+
text(node,value){
|
|
84
|
+
if(!node||node.textContent===value)return
|
|
85
|
+
const interrupted=this.textJobs.has(node),previous=node.textContent,style=root.getComputedStyle?.(node),opacity=style?.opacity||'1',transform=style?.transform||'none'
|
|
86
|
+
this.clearText(node)
|
|
87
|
+
const animate=!!previous&&previous!=='—'&&this.active()&&!this.reduced()&&typeof node.animate==='function'
|
|
88
|
+
let echo
|
|
89
|
+
if(animate){
|
|
90
|
+
echo=node.cloneNode(false);echo.removeAttribute('id');echo.removeAttribute('aria-live');echo.removeAttribute('data-i18n');echo.setAttribute('aria-hidden','true');echo.classList.add('pulse-text-echo');echo.textContent=previous
|
|
91
|
+
Object.assign(echo.style,{left:node.offsetLeft+'px',top:node.offsetTop+'px',width:node.offsetWidth+'px',margin:'0'})
|
|
92
|
+
node.parentElement.appendChild(echo)
|
|
93
|
+
}
|
|
94
|
+
node.textContent=value
|
|
95
|
+
if(!animate)return
|
|
96
|
+
const timing={duration:240,easing:'cubic-bezier(0.23, 1, 0.32, 1)'},job={echo}
|
|
97
|
+
job.enter=node.animate([{opacity:interrupted?opacity:0,transform:transform==='none'?'translateY(3px)':transform},{opacity:1,transform:'translateY(0)'}],timing)
|
|
98
|
+
job.exit=echo.animate([{opacity,transform},{opacity:0,transform:'translateY(-3px)'}],timing)
|
|
99
|
+
this.textJobs.set(node,job)
|
|
100
|
+
Promise.all([job.enter.finished,job.exit.finished]).catch(()=>{}).finally(()=>{if(this.textJobs.get(node)===job){this.textJobs.delete(node);echo.remove()}})
|
|
101
|
+
}
|
|
102
|
+
pause(){
|
|
103
|
+
this.clearTimer(this.settleTimer);this.settleTimer=null;this.checking=false;this.card.dataset.pulseRunning='false'
|
|
104
|
+
for(const node of [...this.textJobs.keys()])this.clearText(node)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if(typeof module!=='undefined'&&module.exports)module.exports={LinkCheck,PulseVisual}
|
|
108
|
+
else root.DshLinkCheck={LinkCheck,PulseVisual}
|
|
109
|
+
})(typeof window==='undefined'?globalThis:window)
|
package/public/plugin-center.css
CHANGED
|
@@ -1 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
/* Shared plugin manager: theme tokens, readable cards and compact mobile controls. */
|
|
2
|
+
.pc-dialog{width:min(920px,94vw);max-height:88dvh;box-sizing:border-box;border:1px solid var(--dsr-line,#30363d);border-radius:20px;background:var(--dsr-bg,#0d1117);color:var(--dsr-text,#eee);padding:26px;overflow:auto}
|
|
3
|
+
.pc-dialog *,.pc-dialog *::before,.pc-dialog *::after{box-sizing:border-box}
|
|
4
|
+
.pc-dialog::backdrop{background:var(--dsr-overlay,#0009)}
|
|
5
|
+
.pc-dialog [hidden]{display:none!important}
|
|
6
|
+
.pc-dialog h2,.pc-dialog h3,.pc-dialog p{margin:0}
|
|
7
|
+
.pc-dialog h2{font-size:26px;letter-spacing:-.5px;line-height:1.3}
|
|
8
|
+
.pc-dialog h3{font-size:16px;line-height:1.4;overflow-wrap:anywhere}
|
|
9
|
+
.pc-dialog p{line-height:1.6;overflow-wrap:anywhere}
|
|
10
|
+
.pc-dialog button,.pc-dialog input,.pc-dialog select{font:inherit;font-size:13px;color:inherit;border:1px solid var(--dsr-line,#30363d);border-radius:10px;padding:10px 13px;background:var(--dsr-panel,#161b22)}
|
|
11
|
+
.pc-dialog button{cursor:pointer;transition:background .15s,border-color .15s}
|
|
12
|
+
.pc-dialog button:hover{background:var(--dsr-panel-2,#21262d);border-color:var(--dsr-muted,#888)}
|
|
13
|
+
.pc-dialog button:disabled{opacity:.45;cursor:not-allowed}
|
|
14
|
+
.pc-dialog :is(button,input,select):focus-visible{outline:2px solid var(--dsr-accent,#58a6ff);outline-offset:3px}
|
|
15
|
+
.pc-dialog input{min-width:0;max-width:100%}
|
|
16
|
+
.pc-dialog a{color:var(--dsr-accent-strong,#79c0ff)}
|
|
17
|
+
.pc-dialog small{font-size:12px;color:var(--dsr-muted,#a7b0bc)}
|
|
18
|
+
.pc-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
|
|
19
|
+
.pc-intro{margin-top:7px!important;font-size:13px;color:var(--dsr-muted,#a7b0bc)}
|
|
20
|
+
.pc-context{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:22px 0}
|
|
21
|
+
.pc-profile{font-size:12px;color:var(--dsr-muted,#a7b0bc)}
|
|
22
|
+
.pc-profile::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--dsr-accent,#58a6ff);margin-right:7px;vertical-align:2px}
|
|
23
|
+
.pc-summary{display:flex;align-items:center;gap:20px}
|
|
24
|
+
.pc-stat{display:inline-flex;align-items:baseline;gap:7px;font-size:12px;color:var(--dsr-muted,#a7b0bc)}
|
|
25
|
+
.pc-stat strong{color:var(--dsr-text,#eee);font-size:20px;font-weight:650;font-variant-numeric:tabular-nums}
|
|
26
|
+
.pc-status,.pc-message:not(:empty),.pc-recovery:not(:empty){border:1px solid var(--dsr-warning-line,#ff9e6466);border-radius:12px;padding:12px 14px;margin:0 0 16px!important;font-size:13px;background:var(--dsr-warning-soft,#ff9e6421);color:var(--dsr-text,#eee)}
|
|
27
|
+
.pc-message:empty,.pc-detail:empty,.pc-recovery:empty{display:none}
|
|
28
|
+
.pc-recovery button{margin-top:10px}
|
|
29
|
+
.pc-controls{border:1px solid var(--dsr-line,#30363d);border-radius:14px;padding:12px;margin-bottom:20px;background:var(--dsr-panel,#161b22)}
|
|
30
|
+
.pc-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
31
|
+
.pc-tabs{display:flex;gap:3px;min-width:0;padding:3px;border-radius:10px;background:var(--dsr-bg-2,#11161e)}
|
|
32
|
+
.pc-tabs button{white-space:nowrap;padding:8px 14px;background:transparent;border-color:transparent;color:var(--dsr-muted,#a7b0bc);border-radius:8px}
|
|
33
|
+
.pc-tabs button.active{background:var(--dsr-accent-soft,#58a6ff1f);color:var(--dsr-accent-strong,#79c0ff)}
|
|
34
|
+
.pc-dialog .pc-refresh{border-color:transparent;background:transparent;color:var(--dsr-muted,#a7b0bc);padding:8px;flex-shrink:0}
|
|
35
|
+
.pc-filters:not([hidden]),.pc-market-tools:not([hidden]){display:flex;gap:10px;margin-top:12px}
|
|
36
|
+
.pc-filters input,.pc-search{flex:1;width:0}
|
|
37
|
+
.pc-filters select{flex-shrink:0;max-width:42%}
|
|
38
|
+
.pc-market-tools{flex-wrap:wrap}
|
|
39
|
+
.pc-market-tools .pc-search{flex-basis:160px}
|
|
40
|
+
.pc-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,310px),1fr));gap:14px}
|
|
41
|
+
.pc-card{min-width:0;border:1px solid var(--dsr-line,#30363d);border-radius:16px;padding:18px;background:var(--dsr-panel,#161b22)}
|
|
42
|
+
.pc-card-head{display:flex;align-items:center;gap:12px;min-height:40px}
|
|
43
|
+
.pc-card-head h3{flex:1;min-width:0}
|
|
44
|
+
.pc-plugin-icon{display:flex;justify-content:center;align-items:center;flex:0 0 40px;height:40px;font-size:17px;font-weight:600;border-radius:12px;background:var(--dsr-accent-soft,#58a6ff1f);color:var(--dsr-accent-strong,#79c0ff)}
|
|
45
|
+
.pc-description{margin-top:14px!important;font-size:13px;color:var(--dsr-muted,#a7b0bc);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
46
|
+
.pc-package{display:block;margin-top:7px;font-family:ui-monospace,monospace;overflow-wrap:anywhere;font-size:12px!important}
|
|
47
|
+
.pc-meta{display:flex;align-items:center;flex-wrap:wrap;gap:7px;margin:14px 0 16px}
|
|
48
|
+
.pc-badge,.pc-version{font-size:11px;border-radius:6px;padding:4px 7px;line-height:1.2;background:var(--dsr-panel-2,#21262d);color:var(--dsr-muted,#a7b0bc)}
|
|
49
|
+
.pc-badge.is-active{background:var(--dsr-success-soft,#4bcf7e21);color:var(--dsr-success,#4bcf7e)}
|
|
50
|
+
.pc-badge.is-error{background:var(--dsr-error-soft,#ff8f6b24);color:var(--dsr-error,#ff8f6b)}
|
|
51
|
+
.pc-dialog .pc-toggle{position:relative;flex:0 0 42px;width:42px;height:26px;min-height:0;padding:0;border-radius:20px;border:1px solid var(--dsr-line,#30363d);background:var(--dsr-panel-2,#21262d);font-size:0}
|
|
52
|
+
.pc-toggle::after{content:'';position:absolute;top:4px;left:4px;width:16px;height:16px;border-radius:50%;background:var(--dsr-muted,#a7b0bc);transition:transform .15s}
|
|
53
|
+
.pc-dialog .pc-toggle[aria-checked=true]{background:var(--dsr-accent,#58a6ff);border-color:var(--dsr-accent,#58a6ff)}
|
|
54
|
+
.pc-toggle[aria-checked=true]::after{transform:translateX(16px);background:var(--dsr-on-accent,#07111f)}
|
|
55
|
+
.pc-card-actions{display:flex;align-items:center;gap:6px;border-top:1px solid var(--dsr-line,#30363d);padding-top:12px;margin-top:auto}
|
|
56
|
+
.pc-card-actions button{border-color:transparent;background:transparent;font-size:12px;padding:8px 10px}
|
|
57
|
+
.pc-card-actions .pc-detail-button{background:var(--dsr-accent-soft,#58a6ff1f);color:var(--dsr-accent-strong,#79c0ff);margin-right:auto}
|
|
58
|
+
.pc-card-actions .pc-danger-button{color:var(--dsr-muted,#a7b0bc)}
|
|
59
|
+
.pc-card-actions .pc-danger-button:hover{background:var(--dsr-error-soft,#ff8f6b24);color:var(--dsr-error,#ff8f6b);border-color:transparent}
|
|
60
|
+
.pc-list>.pc-card{display:flex;flex-direction:column}
|
|
61
|
+
.pc-detail .pc-card{margin-bottom:18px;border-color:var(--dsr-accent-line,#58a6ff66)}
|
|
62
|
+
.pc-detail .pc-description{display:block}
|
|
63
|
+
.pc-detail .pc-card>p,.pc-detail .pc-card>h4{margin:14px 0 0;font-size:13px}
|
|
64
|
+
.pc-detail .pc-card>button,.pc-detail .pc-card>input,.pc-detail .pc-card>a{margin:16px 8px 0 0}
|
|
65
|
+
.pc-dialog button.pc-primary{background:var(--dsr-accent,#58a6ff);color:var(--dsr-on-accent,#07111f);border-color:transparent}
|
|
66
|
+
.pc-dialog button.pc-danger-confirm{background:var(--dsr-error-soft,#ff8f6b24);color:var(--dsr-error,#ff8f6b);border-color:var(--dsr-error-line,#ff8f6b66)}
|
|
67
|
+
.pc-empty{grid-column:1/-1;padding:30px 16px;text-align:center;border:1px dashed var(--dsr-line,#30363d);border-radius:14px;font-size:13px;color:var(--dsr-muted,#a7b0bc)}
|
|
68
|
+
.pc-list>details,.pc-market-note{grid-column:1/-1}
|
|
69
|
+
.pc-market-note{font-size:12px;color:var(--dsr-muted,#a7b0bc);margin-bottom:4px!important}
|
|
70
|
+
.pc-dialog details{font-size:13px;padding:12px 0}
|
|
71
|
+
.pc-dialog summary{cursor:pointer;overflow-wrap:anywhere;color:var(--dsr-muted,#a7b0bc)}
|
|
72
|
+
.pc-runtime,.pc-history{border-top:1px solid var(--dsr-line,#30363d);margin-top:6px}
|
|
73
|
+
.pc-history{margin-top:16px}
|
|
74
|
+
.pc-dialog details p{margin-top:10px;font-size:12px}
|
|
75
|
+
.pc-jobs>details{margin-top:12px;padding:12px;border-radius:10px;background:var(--dsr-panel,#161b22)}
|
|
76
|
+
.pc-dialog pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:240px;overflow:auto;font-size:12px}
|
|
77
|
+
.pc-page{width:100%;max-height:none;border:0;border-radius:0;background:transparent;padding:12px 0 24px;overflow:visible}
|
|
78
|
+
.plugins-view{padding-top:8px}
|
|
79
|
+
@media(max-width:520px){
|
|
80
|
+
.pc-dialog{padding:18px}.pc-page{padding:6px 0 24px}.pc-dialog h2{font-size:24px}
|
|
81
|
+
.pc-context{align-items:flex-start;flex-direction:column;gap:10px;margin:18px 0}.pc-summary{gap:24px}
|
|
82
|
+
.pc-controls{padding:10px;margin-bottom:16px}.pc-toolbar{gap:4px}.pc-tabs{flex:1}
|
|
83
|
+
.pc-tabs button{flex:1;padding:9px 8px;font-size:12px}
|
|
84
|
+
.pc-filters:not([hidden]),.pc-market-tools:not([hidden]){gap:7px}
|
|
85
|
+
.pc-dialog input,.pc-dialog select{font-size:12px;padding:11px 9px}
|
|
86
|
+
.pc-card{padding:16px}.pc-card-actions button{min-height:40px;padding:9px 10px}
|
|
87
|
+
.pc-dialog .pc-toggle::before{content:'';position:absolute;inset:-9px -3px}
|
|
88
|
+
}
|
|
89
|
+
@media(prefers-reduced-motion:reduce){.pc-dialog button,.pc-toggle::after{transition:none}}
|
|
90
|
+
|
|
91
|
+
.pc-inline-detail { margin-top:14px; padding-top:14px; border-top:1px solid var(--dsr-line); overflow-wrap:anywhere; }
|
|
92
|
+
.pc-inline-detail:empty,.pc-inline-detail[hidden] { display:none; }
|
|
93
|
+
.pc-inline-detail p,.pc-inline-detail h4 { margin:10px 0 0; font-size:13px; }
|
|
94
|
+
.pc-inline-detail button,.pc-inline-detail input,.pc-inline-detail a { margin:12px 8px 0 0; }
|
|
95
|
+
.pc-inline-detail input { max-width:100%; box-sizing:border-box; }
|
|
96
|
+
.pc-detail-error { color:var(--dsr-warning); }
|
|
97
|
+
.pc-detail-loading { color:var(--dsr-muted); }
|
|
98
|
+
|
|
99
|
+
.pc-list { align-items:start; }
|
|
100
|
+
.pc-inline-detail button,.pc-card-actions button { min-height:44px; }
|