dsh-remote-plugin 0.7.1 → 0.7.2
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 +9 -1
- package/index.mjs +10 -0
- 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 +100 -174
- package/public/desktop/desktop.html +5 -0
- package/public/desktop/desktop.js +71 -5
- package/public/index.html +14 -64
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/plugin-center.css +89 -1
- package/public/plugin-center.js +148 -39
- package/public/plugin.js +7 -1
- package/public/styles.css +0 -93
- package/public/update.json +8 -8
- package/public/version.json +1 -1
|
@@ -102,6 +102,32 @@ const state = {
|
|
|
102
102
|
view: 'sessions',
|
|
103
103
|
subagentExpandedSession: ''
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
// Increment on every identity transition, including A -> B -> A and token edits.
|
|
107
|
+
let connectionGeneration = 0, sessionsRequest = 0, filesRequest = 0, workbenchRequest = 0
|
|
108
|
+
for (const key of ['server', 'token']) {
|
|
109
|
+
let value = state[key]
|
|
110
|
+
Object.defineProperty(state, key, {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
get: () => value,
|
|
113
|
+
set(next) {
|
|
114
|
+
if (next === value) return
|
|
115
|
+
value = next; connectionGeneration++
|
|
116
|
+
state.sessions = []; state.byId.clear(); state.pendingProjections.clear()
|
|
117
|
+
state.current = null; state.history = emptyDesktopHistory()
|
|
118
|
+
state.fs.path = null; state.fs.initial = null; state.fs.loaded = false; state.fs.roots = []; state.fs.rootIndex = 0
|
|
119
|
+
state.workspaces = []; state.archivedIds = []; state.wb = { bound: false, path: '', projects: null }
|
|
120
|
+
window.DshPluginCenter?.close()
|
|
121
|
+
window.DshInsights?.close()
|
|
122
|
+
if (typeof document !== 'undefined') document.getElementById('modal-workspace')?.classList.add('hidden')
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
function captureConnection() {
|
|
127
|
+
const generation = connectionGeneration, server = state.server, token = state.token
|
|
128
|
+
return { server, token, valid: () => generation === connectionGeneration && server === state.server && token === state.token }
|
|
129
|
+
}
|
|
130
|
+
|
|
105
131
|
const streams = {}
|
|
106
132
|
let pollTimer = null
|
|
107
133
|
let wsRetryTimer = null
|
|
@@ -607,8 +633,10 @@ async function respond(rpcId, value) {
|
|
|
607
633
|
return receipt?.accepted === true
|
|
608
634
|
}
|
|
609
635
|
async function safeRpc(method, payload, errText) {
|
|
636
|
+
const connection = captureConnection()
|
|
610
637
|
try { return await rpc(method, payload) }
|
|
611
638
|
catch (e) {
|
|
639
|
+
if (!connection.valid()) return null
|
|
612
640
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return null }
|
|
613
641
|
toast(errText ? `${errText}:${e.message}` : e.message, 'err')
|
|
614
642
|
return null
|
|
@@ -1512,6 +1540,7 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1512
1540
|
s.projections.asOfSeq = Math.max(currentSeq, seq || 0)
|
|
1513
1541
|
if (state.current === sessionId) {
|
|
1514
1542
|
renderSessions()
|
|
1543
|
+
window.DshInsights?.update()
|
|
1515
1544
|
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
1516
1545
|
}
|
|
1517
1546
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
@@ -1538,6 +1567,10 @@ function resyncAfterStreamOpen() {
|
|
|
1538
1567
|
void refreshSessions().then(() => resyncCurrentSession())
|
|
1539
1568
|
}
|
|
1540
1569
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1570
|
+
function openInsights(tab = 'context') {
|
|
1571
|
+
window.DshInsights?.open({ tab, connection: captureConnection(), url: apiUrl,
|
|
1572
|
+
getSession: () => state.byId.get(state.current), getTitle: () => titleOf(state.byId.get(state.current)) })
|
|
1573
|
+
}
|
|
1541
1574
|
function sessionTitleValue(s) {
|
|
1542
1575
|
const value = proj(s, 'title', '')
|
|
1543
1576
|
return value == null ? '' : String(value).trim()
|
|
@@ -1611,7 +1644,9 @@ function onSessionEvent(sessionId, event) {
|
|
|
1611
1644
|
|
|
1612
1645
|
/* ---------------- 会话 ---------------- */
|
|
1613
1646
|
async function refreshSessions() {
|
|
1647
|
+
const connection = captureConnection(), request = ++sessionsRequest
|
|
1614
1648
|
const v = await safeRpc('session.list', {}, '')
|
|
1649
|
+
if (!connection.valid() || request !== sessionsRequest) return
|
|
1615
1650
|
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1616
1651
|
state.sessions = v.items || []
|
|
1617
1652
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
@@ -1792,6 +1827,7 @@ async function closeSession() {
|
|
|
1792
1827
|
showView('view-sessions')
|
|
1793
1828
|
}
|
|
1794
1829
|
async function loadHistory() {
|
|
1830
|
+
const connection = captureConnection()
|
|
1795
1831
|
const id = state.current
|
|
1796
1832
|
if (!id || state.history.loading) return
|
|
1797
1833
|
const history = state.history
|
|
@@ -1801,14 +1837,14 @@ async function loadHistory() {
|
|
|
1801
1837
|
let v
|
|
1802
1838
|
try { v = await rpc('session.history', { sessionId: id, maxMessages: 60 }) }
|
|
1803
1839
|
catch (e) {
|
|
1804
|
-
if (state.current !== id || state.history !== history) return
|
|
1840
|
+
if (!connection.valid() || state.current !== id || state.history !== history) return
|
|
1805
1841
|
state.history.loading = false
|
|
1806
1842
|
if (e.message === 'AUTH') return
|
|
1807
1843
|
setSessionRecovery('error', e.message)
|
|
1808
1844
|
$('history').innerHTML = `<div class="ds-empty">${e.message}</div>`
|
|
1809
1845
|
return
|
|
1810
1846
|
}
|
|
1811
|
-
if (state.current !== id || state.history !== history) return
|
|
1847
|
+
if (!connection.valid() || state.current !== id || state.history !== history) return
|
|
1812
1848
|
const liveReasoning = (history.reasoningVersion || 0) !== reasoningVersion ? new Map(history.partialReasoning) : null
|
|
1813
1849
|
hydrateSessionProjections(id, v.projections)
|
|
1814
1850
|
for (const entry of v.events || []) {
|
|
@@ -1972,6 +2008,8 @@ function renderHistory() {
|
|
|
1972
2008
|
/* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
|
|
1973
2009
|
let sessionCardsRenderGeneration = 0
|
|
1974
2010
|
async function renderSessionCards() {
|
|
2011
|
+
window.DshInsights?.update()
|
|
2012
|
+
$('btn-insights')?.classList.toggle('hidden', !state.current)
|
|
1975
2013
|
const renderGeneration = ++sessionCardsRenderGeneration
|
|
1976
2014
|
const sessionId = state.current
|
|
1977
2015
|
const box = $('session-cards')
|
|
@@ -2514,8 +2552,10 @@ function resetFsForServerDesktop() {
|
|
|
2514
2552
|
if (select) select.classList.add('hidden')
|
|
2515
2553
|
}
|
|
2516
2554
|
async function openWorkspaceModal() {
|
|
2555
|
+
const connection = captureConnection()
|
|
2517
2556
|
if (!state.token) { toast(t('ds.toastAuth'), 'err'); showView('view-settings'); return }
|
|
2518
2557
|
if (!state.fs.path) await loadFs(null, true)
|
|
2558
|
+
if (!connection.valid()) return
|
|
2519
2559
|
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
2520
2560
|
$('workspace-name').value = ''
|
|
2521
2561
|
$('modal-workspace').classList.remove('hidden')
|
|
@@ -2526,31 +2566,39 @@ async function createWorkspace() {
|
|
|
2526
2566
|
if (createWorkspace.busy) return
|
|
2527
2567
|
const name = $('workspace-name').value.trim()
|
|
2528
2568
|
if (!name) { toast(t('ds.workspaceNameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
2569
|
+
const connection = captureConnection()
|
|
2529
2570
|
createWorkspace.busy = true
|
|
2530
2571
|
const parent = state.fs.path || ''
|
|
2531
2572
|
const button = $('workspace-create')
|
|
2532
2573
|
button.disabled = true
|
|
2533
2574
|
try {
|
|
2534
2575
|
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
2576
|
+
if (!connection.valid()) return
|
|
2535
2577
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2536
2578
|
const data = await res.json().catch(() => ({}))
|
|
2579
|
+
if (!connection.valid()) return
|
|
2537
2580
|
if (!res.ok) {
|
|
2538
2581
|
const msg = data.error === 'exists' ? t('ds.workspaceExists') : data.error === 'bad-name' ? t('ds.workspaceInvalidName') : data.error || ('HTTP ' + res.status)
|
|
2539
2582
|
throw new Error(msg)
|
|
2540
2583
|
}
|
|
2541
2584
|
closeWorkspaceModal()
|
|
2542
2585
|
await loadFs(parent || null, true)
|
|
2586
|
+
if (!connection.valid()) return
|
|
2543
2587
|
let workspace = null
|
|
2544
2588
|
try {
|
|
2545
2589
|
const created = await rpc('workspace.create', { path: data.path })
|
|
2590
|
+
if (!connection.valid()) return
|
|
2546
2591
|
workspace = created?.workspace || null
|
|
2547
2592
|
} catch {}
|
|
2593
|
+
if (!connection.valid()) return
|
|
2548
2594
|
if (workspace?.workspaceId) {
|
|
2549
2595
|
state.workspaces = [...state.workspaces.filter(item => item.workspaceId !== workspace.workspaceId), workspace]
|
|
2550
2596
|
}
|
|
2551
2597
|
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
2552
2598
|
const v = await safeRpc('session.create', sessionPayload, t('ds.toastOpFailed'))
|
|
2599
|
+
if (!connection.valid()) return
|
|
2553
2600
|
await refreshSessions()
|
|
2601
|
+
if (!connection.valid()) return
|
|
2554
2602
|
if (v?.sessionId) {
|
|
2555
2603
|
if (workspace?.workspaceId) LS.set('lastNewSessionWorkspaceV1', workspace.workspaceId)
|
|
2556
2604
|
toast(t('ds.workspaceCreated'), 'ok')
|
|
@@ -2559,6 +2607,7 @@ async function createWorkspace() {
|
|
|
2559
2607
|
toast(t('ds.workspaceCreatedNoSession'), 'ok')
|
|
2560
2608
|
}
|
|
2561
2609
|
} catch (e) {
|
|
2610
|
+
if (!connection.valid()) return
|
|
2562
2611
|
toast(`${t('ds.workspaceCreateFailed')}:${e.message || t('ds.feedbackNetworkError')}`, 'err')
|
|
2563
2612
|
} finally {
|
|
2564
2613
|
createWorkspace.busy = false
|
|
@@ -2566,6 +2615,8 @@ async function createWorkspace() {
|
|
|
2566
2615
|
}
|
|
2567
2616
|
}
|
|
2568
2617
|
async function loadFs(dir, silent) {
|
|
2618
|
+
const connection = captureConnection(), request = ++filesRequest
|
|
2619
|
+
const valid = () => connection.valid() && request === filesRequest
|
|
2569
2620
|
if (!state.token) {
|
|
2570
2621
|
$('fs-path').textContent = t('ds.toastAuth')
|
|
2571
2622
|
$('fs-list').innerHTML = `<div class="ds-empty">${t('ds.toastAuth')}</div>`
|
|
@@ -2578,8 +2629,10 @@ async function loadFs(dir, silent) {
|
|
|
2578
2629
|
}
|
|
2579
2630
|
try {
|
|
2580
2631
|
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
2632
|
+
if (!valid()) return
|
|
2581
2633
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2582
2634
|
const data = await res.json().catch(() => ({}))
|
|
2635
|
+
if (!valid()) return
|
|
2583
2636
|
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
2584
2637
|
state.fs.path = data.path
|
|
2585
2638
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
@@ -2601,6 +2654,7 @@ async function loadFs(dir, silent) {
|
|
|
2601
2654
|
else window.open(fsApiUrl('/file', { path: row.dataset.fsPath, token: state.token }), '_blank')
|
|
2602
2655
|
}))
|
|
2603
2656
|
} catch (e) {
|
|
2657
|
+
if (!valid()) return
|
|
2604
2658
|
$('fs-path').textContent = target || '~'
|
|
2605
2659
|
$('fs-list').innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
2606
2660
|
}
|
|
@@ -2742,17 +2796,22 @@ function commitWorkspaceSessionOrder(workspaceId, order) {
|
|
|
2742
2796
|
toast(t('ds.wbOrderSaved'), 'ok')
|
|
2743
2797
|
}
|
|
2744
2798
|
async function refreshWorkbench({ silent = false } = {}) {
|
|
2799
|
+
const connection = captureConnection(), request = ++workbenchRequest
|
|
2800
|
+
const valid = () => connection.valid() && request === workbenchRequest
|
|
2745
2801
|
if (!state.token) { renderWorkbench(); return }
|
|
2746
2802
|
let wb = null
|
|
2747
2803
|
try {
|
|
2748
2804
|
wb = await wbGateway('GET', '/workbench')
|
|
2805
|
+
if (!valid()) return
|
|
2749
2806
|
state.wb.apiMissing = false
|
|
2750
2807
|
} catch (e) {
|
|
2808
|
+
if (!valid()) return
|
|
2751
2809
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
2752
2810
|
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
2753
2811
|
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
2754
2812
|
}
|
|
2755
2813
|
const wl = await safeRpc('workspace.list', {}, '')
|
|
2814
|
+
if (!valid()) return
|
|
2756
2815
|
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
2757
2816
|
state.workspaces = wl && Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2758
2817
|
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
@@ -2769,8 +2828,10 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2769
2828
|
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2770
2829
|
try {
|
|
2771
2830
|
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
2831
|
+
if (!valid()) return
|
|
2772
2832
|
if (listRes.ok) {
|
|
2773
2833
|
const listData = await listRes.json().catch(() => ({}))
|
|
2834
|
+
if (!valid()) return
|
|
2774
2835
|
if (Array.isArray(listData.entries)) {
|
|
2775
2836
|
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
2776
2837
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
@@ -2783,16 +2844,19 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2783
2844
|
if (have.has(wbPathKey(projectPath))) continue
|
|
2784
2845
|
try {
|
|
2785
2846
|
const created = await rpc('workspace.create', { path: projectPath })
|
|
2847
|
+
if (!valid()) return
|
|
2786
2848
|
if (created?.workspace) {
|
|
2787
2849
|
items.push(created.workspace)
|
|
2788
2850
|
state.workspaces.push(created.workspace)
|
|
2789
2851
|
have.add(wbPathKey(projectPath))
|
|
2790
2852
|
}
|
|
2791
2853
|
} catch {}
|
|
2854
|
+
if (!valid()) return
|
|
2792
2855
|
}
|
|
2793
2856
|
}
|
|
2794
2857
|
}
|
|
2795
2858
|
} catch {}
|
|
2859
|
+
if (!valid()) return
|
|
2796
2860
|
state.wb.projects = orderedWorkspaceItems(items
|
|
2797
2861
|
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
2798
2862
|
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true })))
|
|
@@ -3369,6 +3433,7 @@ function bindUi() {
|
|
|
3369
3433
|
if (!e.target.closest('#model-menu') && !e.target.closest('#btn-model')) $('model-menu')?.classList.add('hidden')
|
|
3370
3434
|
})
|
|
3371
3435
|
$('btn-stats-top').addEventListener('click', toggleStatsDrawer)
|
|
3436
|
+
$('btn-insights').addEventListener('click', () => openInsights())
|
|
3372
3437
|
$('stats-drawer-close').addEventListener('click', toggleStatsDrawer)
|
|
3373
3438
|
// 赞赏支持
|
|
3374
3439
|
$('btn-donate').addEventListener('click', openDonateModal)
|
|
@@ -3523,11 +3588,12 @@ start()
|
|
|
3523
3588
|
|
|
3524
3589
|
// Capture the connection so switching servers cannot redirect a plugin mutation.
|
|
3525
3590
|
document.getElementById('btn-plugin-center')?.addEventListener('click', () => {
|
|
3526
|
-
const
|
|
3527
|
-
const
|
|
3591
|
+
const connection = captureConnection()
|
|
3592
|
+
const server = connection.server || ''
|
|
3593
|
+
const token = connection.token
|
|
3528
3594
|
window.DshPluginCenter.open({
|
|
3529
3595
|
url: path => server + path,
|
|
3530
3596
|
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
3531
|
-
valid:
|
|
3597
|
+
valid: connection.valid,
|
|
3532
3598
|
})
|
|
3533
3599
|
})
|
package/public/index.html
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<link rel="stylesheet" href="styles.css">
|
|
15
15
|
<link rel="stylesheet" href="genui.css">
|
|
16
16
|
<link rel="stylesheet" href="plugin-center.css">
|
|
17
|
+
<link rel="stylesheet" href="insights.css">
|
|
17
18
|
</head>
|
|
18
19
|
<body>
|
|
19
20
|
<header class="topbar">
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
<button id="btn-rename-session" class="icon-btn hidden" data-i18n-title="session.rename" data-i18n-aria="session.rename">✎</button>
|
|
114
115
|
<button id="btn-archive-session" class="mini-btn hidden" data-i18n="session.archive">归档</button>
|
|
115
116
|
<button id="btn-stats" class="icon-btn" data-i18n-title="stats.title" data-i18n-aria="stats.title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19V9M12 19V5M19 19v-7M3 19h18"/></svg></button>
|
|
117
|
+
<button id="btn-insights" class="icon-btn" data-i18n-title="insights.title" data-i18n-aria="insights.title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4v8l6 5M12 12H4"/></svg></button>
|
|
116
118
|
<button id="btn-fs-send" class="session-send-btn hidden" type="button">发送</button>
|
|
117
119
|
<button id="btn-cancel" class="danger-btn hidden" data-i18n="session.stop">停止</button>
|
|
118
120
|
</div>
|
|
@@ -248,54 +250,15 @@
|
|
|
248
250
|
<div class="overview-legacy hidden" aria-hidden="true"><div id="pending-list"></div><div id="jobs-list"></div><span id="pending-count"></span></div>
|
|
249
251
|
</section>
|
|
250
252
|
|
|
251
|
-
<!--
|
|
252
|
-
<section id="view-
|
|
253
|
-
<
|
|
254
|
-
<div class="stats-page-copy">
|
|
255
|
-
<div class="stats-eyebrow" data-i18n="statsPage.eyebrow">USAGE OVERVIEW</div>
|
|
256
|
-
<h1 id="stats-page-title" data-i18n="statsPage.title">Token 统计</h1>
|
|
257
|
-
<p class="stats-page-subtitle" data-i18n="statsPage.subtitle">按日查看 Token 用量与费用估算</p>
|
|
258
|
-
</div>
|
|
259
|
-
<div class="stats-date-wrap">
|
|
260
|
-
<span class="stats-date-label" data-i18n="statsPage.latest">最新日期</span>
|
|
261
|
-
<span id="stats-sub" class="stats-date">—</span>
|
|
262
|
-
</div>
|
|
263
|
-
</header>
|
|
264
|
-
<div class="stats-toolbar">
|
|
265
|
-
<span class="stats-toolbar-label" data-i18n="statsPage.chartMetric">图表指标</span>
|
|
266
|
-
<div id="stats-mode-picker" class="stats-mode-picker" role="group" data-i18n-aria="statsPage.chartMetric">
|
|
267
|
-
<button type="button" class="stats-mode-btn active" data-stats-mode="token" aria-pressed="true" data-i18n="statsPage.modeTokens">Token 数</button>
|
|
268
|
-
<button type="button" class="stats-mode-btn" data-stats-mode="cost" aria-pressed="false" data-i18n="statsPage.modeCost">费用(官方)</button>
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
<div class="stats-cards" id="stats-cards"></div>
|
|
272
|
-
<section class="stats-panel" data-stats-panel="cost" aria-labelledby="stats-cost-title">
|
|
273
|
-
<div class="stats-panel-head">
|
|
274
|
-
<div>
|
|
275
|
-
<div class="stats-panel-kicker" data-i18n="statsPage.costKicker">ESTIMATED COST</div>
|
|
276
|
-
<h2 id="stats-cost-title" class="stats-panel-title" data-i18n="statsPage.costTrend">费用趋势</h2>
|
|
277
|
-
</div>
|
|
278
|
-
<div id="stats-legend" class="stats-legend"></div>
|
|
279
|
-
</div>
|
|
280
|
-
<div id="stats-chart" class="stats-chart" role="list" aria-live="polite" data-i18n-aria="statsPage.costChartLabel"></div>
|
|
281
|
-
</section>
|
|
282
|
-
<section class="stats-panel" data-stats-panel="token" aria-labelledby="stats-token-title">
|
|
283
|
-
<div class="stats-panel-head">
|
|
284
|
-
<div>
|
|
285
|
-
<div class="stats-panel-kicker" data-i18n="statsPage.tokenKicker">TOKEN VOLUME</div>
|
|
286
|
-
<h2 id="stats-token-title" class="stats-panel-title" data-i18n="statsPage.tokenTrend">Token 量趋势</h2>
|
|
287
|
-
</div>
|
|
288
|
-
<div id="stats-token-legend" class="stats-legend"></div>
|
|
289
|
-
</div>
|
|
290
|
-
<div id="stats-token-chart" class="stats-chart" role="list" aria-live="polite" data-i18n-aria="statsPage.tokenChartLabel"></div>
|
|
291
|
-
</section>
|
|
292
|
-
<p id="stats-note" class="stats-note"></p>
|
|
253
|
+
<!-- 插件管理:独立页面,与当前连接绑定 -->
|
|
254
|
+
<section id="view-plugins" class="view hidden plugins-view" aria-label="插件管理">
|
|
255
|
+
<div id="plugin-page"></div>
|
|
293
256
|
</section>
|
|
294
257
|
|
|
295
258
|
<!-- 设置 -->
|
|
296
259
|
<section id="view-settings" class="view hidden">
|
|
297
260
|
<div id="settings-home">
|
|
298
|
-
<button type="button" id="btn-plugin-center" class="setting-row setting-link"><span
|
|
261
|
+
<button type="button" id="btn-plugin-center" class="setting-row setting-link"><span data-i18n="nav.plugins">插件</span></button>
|
|
299
262
|
<div class="settings-group">
|
|
300
263
|
<button type="button" class="setting-row setting-link" data-settings-group="general">
|
|
301
264
|
<div><div class="setting-name" data-i18n="settings.groupGeneral">通用</div><div class="setting-desc" data-i18n="settings.groupGeneralDesc">显示工具调用、预设提示词</div></div>
|
|
@@ -619,9 +582,9 @@
|
|
|
619
582
|
<span class="nav-home-orb"><span class="nav-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="7.5"/><circle cx="12" cy="12" r="2.2" fill="currentColor" stroke="none"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3" opacity=".75"/></svg></span></span>
|
|
620
583
|
<span data-i18n="nav.home">主页</span><b id="nav-pending" class="nav-badge hidden"></b>
|
|
621
584
|
</button>
|
|
622
|
-
<button data-view="view-
|
|
623
|
-
<span class="nav-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="
|
|
624
|
-
<span data-i18n="nav.
|
|
585
|
+
<button data-view="view-plugins" class="nav-btn">
|
|
586
|
+
<span class="nav-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3v4h6V3M7 7h10v6a5 5 0 0 1-10 0V7ZM12 18v3"/></svg></span>
|
|
587
|
+
<span data-i18n="nav.plugins">插件</span>
|
|
625
588
|
</button>
|
|
626
589
|
<button data-view="view-settings" class="nav-btn">
|
|
627
590
|
<span class="nav-ico"><svg class="settings-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.73v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.73l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/><circle cx="12" cy="12" r="3"/></svg></span>
|
|
@@ -936,7 +899,7 @@
|
|
|
936
899
|
'conn.poll': '轮询', 'conn.pollTitle': '当前网络不支持实时推送,已降级为轮询(延迟数秒)',
|
|
937
900
|
'conn.titleGroup': '{group} · {url}({ms})',
|
|
938
901
|
'common.refreshing': '刷新中…', 'select.choose': '请选择', 'select.cancel': '取消',
|
|
939
|
-
'nav.sessions': '会话', 'nav.files': '文件', 'nav.pending': '待办', 'nav.overview': '总览', 'nav.home': '主页', 'nav.
|
|
902
|
+
'nav.sessions': '会话', 'nav.files': '文件', 'nav.pending': '待办', 'nav.overview': '总览', 'nav.home': '主页', 'nav.plugins': '插件', 'nav.settings': '设置',
|
|
940
903
|
'overview.eyebrow': 'SYSTEM OVERVIEW', 'overview.title': '主页', 'overview.subtitle': '实时状态与需要你处理的事项',
|
|
941
904
|
'overview.systemPulse': '系统链路', 'overview.nominal': '系统链路正常', 'overview.degraded': '部分链路需要关注', 'overview.offline': '连接已断开', 'overview.checking': '检查中…', 'overview.components': '{n}/4 个模块在线', 'overview.live': 'LIVE', 'overview.allLinked': '全部链路', 'overview.offlineCore': 'OFFLINE',
|
|
942
905
|
'overview.gateway': '网关', 'overview.dsh': 'DSH 上游', 'overview.mux': '实时 mux', 'overview.host': '实时 host', 'overview.online': '在线', 'overview.offlineShort': '离线',
|
|
@@ -946,14 +909,6 @@
|
|
|
946
909
|
'overview.action.openSession': '打开最近会话', 'overview.action.newSession': '新建会话', 'overview.action.attention': '处理待办', 'overview.action.connect': '去设置连接', 'overview.action.refresh': '重新检查连接',
|
|
947
910
|
'overview.communityEyebrow': 'COMMUNITY BOARD', 'overview.communityTitle': '公告与投票', 'overview.communityHistory': '历史公告', 'overview.communityEmpty': '暂无未读公告',
|
|
948
911
|
'overview.communityPoll': '投票', 'overview.communityNotice': '公告', 'overview.communityNew': '新', 'overview.communityVote': '参与投票', 'overview.communityView': '查看公告', 'overview.communityVoted': '已参与',
|
|
949
|
-
'statsPage.eyebrow': '使用概览', 'statsPage.title': 'Token 统计', 'statsPage.subtitle': '按日查看 Token 用量与费用估算', 'statsPage.latest': '最新日期', 'statsPage.chartMetric': '图表指标', 'statsPage.modeTokens': 'Token 数', 'statsPage.modeCost': '费用(官方)',
|
|
950
|
-
'statsPage.costKicker': 'ESTIMATED COST', 'statsPage.tokenKicker': 'TOKEN VOLUME', 'statsPage.costChartLabel': '近 7 天费用趋势', 'statsPage.tokenChartLabel': '近 7 天 Token 用量趋势',
|
|
951
|
-
'statsPage.today': '今日', 'statsPage.tokenUnit': 'Tokens', 'statsPage.breakdown': '今日用量构成', 'statsPage.estimated': '估算费用', 'statsPage.peakShareLabel': '高峰费用占比 {n}%',
|
|
952
|
-
'statsPage.todayTokens': '今日 Token', 'statsPage.todayCost': '今日费用', 'statsPage.peakShare': '高峰占比', 'statsPage.costTrend': '费用趋势', 'statsPage.tokenTrend': 'Token 量趋势', 'statsPage.tokenTotal': 'Token 总量',
|
|
953
|
-
'statsPage.input': '未缓存输入', 'statsPage.cacheRead': '缓存命中', 'statsPage.cacheWrite': '缓存写入', 'statsPage.output': '输出',
|
|
954
|
-
'statsPage.peak': '高峰', 'statsPage.off': '空闲', 'statsPage.days': '近 {n} 天',
|
|
955
|
-
'statsPage.gatewayDown': '统计需要网关运行', 'statsPage.empty': '暂无统计,产生会话后自动聚合',
|
|
956
|
-
'statsPage.note': '注:Token 量按 DSH usage 事件记录,第三方 provider 需提供 usage 才能计入;费用仅对已知 DeepSeek 模型估算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
957
912
|
'home.newSession': '+ 新会话', 'home.newWorkspace': '+ 工作区', 'home.empty': '暂无会话', 'home.createFailed': '新建会话失败', 'home.created': '会话已创建',
|
|
958
913
|
'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.orderSaved': '顺序已保存到当前设备', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
|
|
959
914
|
'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.exists': '该目录已存在', 'workspace.invalidName': '名称不能包含路径分隔符', 'workspace.createFailed': '创建工作区失败:{msg}', 'workspace.created': '工作区已创建', 'workspace.createdNoSession': '工作区已创建,但新会话未能打开',
|
|
@@ -1007,6 +962,7 @@
|
|
|
1007
962
|
'event.systemReminder': '系统提示',
|
|
1008
963
|
'block.image': '图片', 'block.thinking': '🧠 思考过程', 'block.thinkingLive': '🧠 思考中…', 'block.toolCall': '工具调用', 'block.toolResult': '工具结果', 'block.unknown': '块 · {type}',
|
|
1009
964
|
'truncated': '…(截断)',
|
|
965
|
+
'insights.title': '上下文与费用',
|
|
1010
966
|
'stats.title': '本轮统计', 'stats.roundTitle': '本轮统计', 'stats.turnsSteps': '轮次 / 步骤',
|
|
1011
967
|
'stats.llmTime': '模型耗时', 'stats.minutes': ' 分钟', 'stats.outputCache': '输出 / 缓存读',
|
|
1012
968
|
'stats.ctxPressure': '上下文压力', 'stats.permission': '权限', 'stats.empty': '暂无统计数据',
|
|
@@ -1179,7 +1135,7 @@
|
|
|
1179
1135
|
'conn.poll': 'Polling', 'conn.pollTitle': 'Realtime push is unavailable on this network; degraded to polling (a few seconds delay)',
|
|
1180
1136
|
'conn.titleGroup': '{group} · {url} ({ms})',
|
|
1181
1137
|
'common.refreshing': 'Refreshing…', 'select.choose': 'Choose an option', 'select.cancel': 'Cancel',
|
|
1182
|
-
'nav.sessions': 'Sessions', 'nav.files': 'Files', 'nav.pending': 'Inbox', 'nav.overview': 'Overview', 'nav.home': 'Home', 'nav.
|
|
1138
|
+
'nav.sessions': 'Sessions', 'nav.files': 'Files', 'nav.pending': 'Inbox', 'nav.overview': 'Overview', 'nav.home': 'Home', 'nav.plugins': 'Plugins', 'nav.settings': 'Settings',
|
|
1183
1139
|
'overview.eyebrow': 'SYSTEM OVERVIEW', 'overview.title': 'Home', 'overview.subtitle': 'Live status and items needing your attention',
|
|
1184
1140
|
'overview.systemPulse': 'System pulse', 'overview.nominal': 'System nominal', 'overview.degraded': 'Some links need attention', 'overview.offline': 'Connection offline', 'overview.checking': 'Checking…', 'overview.components': '{n}/4 components online', 'overview.live': 'LIVE', 'overview.allLinked': 'ALL LINKED', 'overview.offlineCore': 'OFFLINE',
|
|
1185
1141
|
'overview.gateway': 'Gateway', 'overview.dsh': 'DSH upstream', 'overview.mux': 'Live mux', 'overview.host': 'Live host', 'overview.online': 'Online', 'overview.offlineShort': 'Offline',
|
|
@@ -1189,14 +1145,6 @@
|
|
|
1189
1145
|
'overview.action.openSession': 'Open recent session', 'overview.action.newSession': 'New session', 'overview.action.attention': 'Review pending', 'overview.action.connect': 'Set up connection', 'overview.action.refresh': 'Check connection',
|
|
1190
1146
|
'overview.communityEyebrow': 'COMMUNITY BOARD', 'overview.communityTitle': 'Announcements & polls', 'overview.communityHistory': 'History', 'overview.communityEmpty': 'No unread announcements',
|
|
1191
1147
|
'overview.communityPoll': 'Poll', 'overview.communityNotice': 'Notice', 'overview.communityNew': 'New', 'overview.communityVote': 'Vote now', 'overview.communityView': 'View notice', 'overview.communityVoted': 'Voted',
|
|
1192
|
-
'statsPage.eyebrow': 'USAGE OVERVIEW', 'statsPage.title': 'Token stats', 'statsPage.subtitle': 'Daily token volume and estimated costs', 'statsPage.latest': 'Latest date', 'statsPage.chartMetric': 'Chart metric', 'statsPage.modeTokens': 'Token count', 'statsPage.modeCost': 'Cost (official)',
|
|
1193
|
-
'statsPage.costKicker': 'ESTIMATED COST', 'statsPage.tokenKicker': 'TOKEN VOLUME', 'statsPage.costChartLabel': 'Cost trend for the last 7 days', 'statsPage.tokenChartLabel': 'Token volume trend for the last 7 days',
|
|
1194
|
-
'statsPage.today': 'Today', 'statsPage.tokenUnit': 'Tokens', 'statsPage.breakdown': 'Today’s usage breakdown', 'statsPage.estimated': 'Estimated cost', 'statsPage.peakShareLabel': 'Peak cost share {n}%',
|
|
1195
|
-
'statsPage.todayTokens': 'Tokens today', 'statsPage.todayCost': 'Cost today', 'statsPage.peakShare': 'Peak share', 'statsPage.costTrend': 'Cost trend', 'statsPage.tokenTrend': 'Token volume trend', 'statsPage.tokenTotal': 'Total tokens',
|
|
1196
|
-
'statsPage.input': 'Uncached input', 'statsPage.cacheRead': 'Cache read', 'statsPage.cacheWrite': 'Cache write', 'statsPage.output': 'Output',
|
|
1197
|
-
'statsPage.peak': 'Peak', 'statsPage.off': 'Off-peak', 'statsPage.days': 'Last {n} days',
|
|
1198
|
-
'statsPage.gatewayDown': 'Stats require the gateway', 'statsPage.empty': 'No stats yet — they aggregate as sessions happen',
|
|
1199
|
-
'statsPage.note': 'Note: token volume uses DSH usage events; a third-party provider must expose usage to be counted. Costs are estimates for known DeepSeek models and may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
|
|
1200
1148
|
'home.newSession': '+ New session', 'home.newWorkspace': '+ Workspace', 'home.empty': 'No sessions yet', 'home.createFailed': 'Failed to create session', 'home.created': 'Session created',
|
|
1201
1149
|
'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.orderSaved': 'Order saved on this device', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
|
|
1202
1150
|
'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.exists': 'That folder already exists', 'workspace.invalidName': 'The name cannot contain path separators', 'workspace.createFailed': 'Could not create workspace: {msg}', 'workspace.created': 'Workspace created', 'workspace.createdNoSession': 'Workspace created, but the new session could not be opened',
|
|
@@ -1250,6 +1198,7 @@
|
|
|
1250
1198
|
'event.systemReminder': 'System reminder',
|
|
1251
1199
|
'block.image': 'Image', 'block.thinking': '🧠 Thinking', 'block.thinkingLive': '🧠 Thinking…', 'block.toolCall': 'Tool call', 'block.toolResult': 'Tool result', 'block.unknown': 'Block · {type}',
|
|
1252
1200
|
'truncated': '…(truncated)',
|
|
1201
|
+
'insights.title': 'Context & costs',
|
|
1253
1202
|
'stats.title': 'This round', 'stats.roundTitle': 'This round', 'stats.turnsSteps': 'Turns / steps',
|
|
1254
1203
|
'stats.llmTime': 'LLM time', 'stats.minutes': ' min', 'stats.outputCache': 'Output / cache read',
|
|
1255
1204
|
'stats.ctxPressure': 'Context pressure', 'stats.permission': 'Permission', 'stats.empty': 'No stats yet',
|
|
@@ -1416,6 +1365,7 @@
|
|
|
1416
1365
|
<script src="motion.js"></script>
|
|
1417
1366
|
<script type="module" src="morphicons-init.js"></script>
|
|
1418
1367
|
<script src="plugin-center.js"></script>
|
|
1368
|
+
<script src="insights.js"></script>
|
|
1419
1369
|
<script src="app.js"></script>
|
|
1420
1370
|
</body>
|
|
1421
1371
|
</html>
|
|
@@ -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)}
|