dsh-remote-plugin 0.7.0 → 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/client.js +12 -2
- package/gateway.cjs +81 -9
- 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 +76 -8
- 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 +12 -12
- 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
|
|
@@ -501,9 +527,11 @@ function closeNotesModal() {
|
|
|
501
527
|
}
|
|
502
528
|
async function checkNotesOnStart() {
|
|
503
529
|
try {
|
|
504
|
-
const res = await fetch('../update.json?t=' + Date.now())
|
|
505
|
-
if (!res.ok) return
|
|
506
|
-
|
|
530
|
+
const res = await fetch('../update.json?t=' + Date.now(), { cache: 'no-store' })
|
|
531
|
+
if (!res.ok || res.redirected || !String(res.headers.get('content-type') || '').includes('application/json')) return
|
|
532
|
+
const info = await res.json()
|
|
533
|
+
if (!info || typeof info !== 'object' || !info.version) return
|
|
534
|
+
openNotesModal(info)
|
|
507
535
|
} catch {}
|
|
508
536
|
}
|
|
509
537
|
async function submitFeedback() {
|
|
@@ -605,8 +633,10 @@ async function respond(rpcId, value) {
|
|
|
605
633
|
return receipt?.accepted === true
|
|
606
634
|
}
|
|
607
635
|
async function safeRpc(method, payload, errText) {
|
|
636
|
+
const connection = captureConnection()
|
|
608
637
|
try { return await rpc(method, payload) }
|
|
609
638
|
catch (e) {
|
|
639
|
+
if (!connection.valid()) return null
|
|
610
640
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return null }
|
|
611
641
|
toast(errText ? `${errText}:${e.message}` : e.message, 'err')
|
|
612
642
|
return null
|
|
@@ -1510,6 +1540,7 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1510
1540
|
s.projections.asOfSeq = Math.max(currentSeq, seq || 0)
|
|
1511
1541
|
if (state.current === sessionId) {
|
|
1512
1542
|
renderSessions()
|
|
1543
|
+
window.DshInsights?.update()
|
|
1513
1544
|
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
1514
1545
|
}
|
|
1515
1546
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
@@ -1536,6 +1567,10 @@ function resyncAfterStreamOpen() {
|
|
|
1536
1567
|
void refreshSessions().then(() => resyncCurrentSession())
|
|
1537
1568
|
}
|
|
1538
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
|
+
}
|
|
1539
1574
|
function sessionTitleValue(s) {
|
|
1540
1575
|
const value = proj(s, 'title', '')
|
|
1541
1576
|
return value == null ? '' : String(value).trim()
|
|
@@ -1609,7 +1644,9 @@ function onSessionEvent(sessionId, event) {
|
|
|
1609
1644
|
|
|
1610
1645
|
/* ---------------- 会话 ---------------- */
|
|
1611
1646
|
async function refreshSessions() {
|
|
1647
|
+
const connection = captureConnection(), request = ++sessionsRequest
|
|
1612
1648
|
const v = await safeRpc('session.list', {}, '')
|
|
1649
|
+
if (!connection.valid() || request !== sessionsRequest) return
|
|
1613
1650
|
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1614
1651
|
state.sessions = v.items || []
|
|
1615
1652
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
@@ -1790,6 +1827,7 @@ async function closeSession() {
|
|
|
1790
1827
|
showView('view-sessions')
|
|
1791
1828
|
}
|
|
1792
1829
|
async function loadHistory() {
|
|
1830
|
+
const connection = captureConnection()
|
|
1793
1831
|
const id = state.current
|
|
1794
1832
|
if (!id || state.history.loading) return
|
|
1795
1833
|
const history = state.history
|
|
@@ -1799,14 +1837,14 @@ async function loadHistory() {
|
|
|
1799
1837
|
let v
|
|
1800
1838
|
try { v = await rpc('session.history', { sessionId: id, maxMessages: 60 }) }
|
|
1801
1839
|
catch (e) {
|
|
1802
|
-
if (state.current !== id || state.history !== history) return
|
|
1840
|
+
if (!connection.valid() || state.current !== id || state.history !== history) return
|
|
1803
1841
|
state.history.loading = false
|
|
1804
1842
|
if (e.message === 'AUTH') return
|
|
1805
1843
|
setSessionRecovery('error', e.message)
|
|
1806
1844
|
$('history').innerHTML = `<div class="ds-empty">${e.message}</div>`
|
|
1807
1845
|
return
|
|
1808
1846
|
}
|
|
1809
|
-
if (state.current !== id || state.history !== history) return
|
|
1847
|
+
if (!connection.valid() || state.current !== id || state.history !== history) return
|
|
1810
1848
|
const liveReasoning = (history.reasoningVersion || 0) !== reasoningVersion ? new Map(history.partialReasoning) : null
|
|
1811
1849
|
hydrateSessionProjections(id, v.projections)
|
|
1812
1850
|
for (const entry of v.events || []) {
|
|
@@ -1970,6 +2008,8 @@ function renderHistory() {
|
|
|
1970
2008
|
/* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
|
|
1971
2009
|
let sessionCardsRenderGeneration = 0
|
|
1972
2010
|
async function renderSessionCards() {
|
|
2011
|
+
window.DshInsights?.update()
|
|
2012
|
+
$('btn-insights')?.classList.toggle('hidden', !state.current)
|
|
1973
2013
|
const renderGeneration = ++sessionCardsRenderGeneration
|
|
1974
2014
|
const sessionId = state.current
|
|
1975
2015
|
const box = $('session-cards')
|
|
@@ -2512,8 +2552,10 @@ function resetFsForServerDesktop() {
|
|
|
2512
2552
|
if (select) select.classList.add('hidden')
|
|
2513
2553
|
}
|
|
2514
2554
|
async function openWorkspaceModal() {
|
|
2555
|
+
const connection = captureConnection()
|
|
2515
2556
|
if (!state.token) { toast(t('ds.toastAuth'), 'err'); showView('view-settings'); return }
|
|
2516
2557
|
if (!state.fs.path) await loadFs(null, true)
|
|
2558
|
+
if (!connection.valid()) return
|
|
2517
2559
|
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
2518
2560
|
$('workspace-name').value = ''
|
|
2519
2561
|
$('modal-workspace').classList.remove('hidden')
|
|
@@ -2524,31 +2566,39 @@ async function createWorkspace() {
|
|
|
2524
2566
|
if (createWorkspace.busy) return
|
|
2525
2567
|
const name = $('workspace-name').value.trim()
|
|
2526
2568
|
if (!name) { toast(t('ds.workspaceNameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
2569
|
+
const connection = captureConnection()
|
|
2527
2570
|
createWorkspace.busy = true
|
|
2528
2571
|
const parent = state.fs.path || ''
|
|
2529
2572
|
const button = $('workspace-create')
|
|
2530
2573
|
button.disabled = true
|
|
2531
2574
|
try {
|
|
2532
2575
|
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
2576
|
+
if (!connection.valid()) return
|
|
2533
2577
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2534
2578
|
const data = await res.json().catch(() => ({}))
|
|
2579
|
+
if (!connection.valid()) return
|
|
2535
2580
|
if (!res.ok) {
|
|
2536
2581
|
const msg = data.error === 'exists' ? t('ds.workspaceExists') : data.error === 'bad-name' ? t('ds.workspaceInvalidName') : data.error || ('HTTP ' + res.status)
|
|
2537
2582
|
throw new Error(msg)
|
|
2538
2583
|
}
|
|
2539
2584
|
closeWorkspaceModal()
|
|
2540
2585
|
await loadFs(parent || null, true)
|
|
2586
|
+
if (!connection.valid()) return
|
|
2541
2587
|
let workspace = null
|
|
2542
2588
|
try {
|
|
2543
2589
|
const created = await rpc('workspace.create', { path: data.path })
|
|
2590
|
+
if (!connection.valid()) return
|
|
2544
2591
|
workspace = created?.workspace || null
|
|
2545
2592
|
} catch {}
|
|
2593
|
+
if (!connection.valid()) return
|
|
2546
2594
|
if (workspace?.workspaceId) {
|
|
2547
2595
|
state.workspaces = [...state.workspaces.filter(item => item.workspaceId !== workspace.workspaceId), workspace]
|
|
2548
2596
|
}
|
|
2549
2597
|
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
2550
2598
|
const v = await safeRpc('session.create', sessionPayload, t('ds.toastOpFailed'))
|
|
2599
|
+
if (!connection.valid()) return
|
|
2551
2600
|
await refreshSessions()
|
|
2601
|
+
if (!connection.valid()) return
|
|
2552
2602
|
if (v?.sessionId) {
|
|
2553
2603
|
if (workspace?.workspaceId) LS.set('lastNewSessionWorkspaceV1', workspace.workspaceId)
|
|
2554
2604
|
toast(t('ds.workspaceCreated'), 'ok')
|
|
@@ -2557,6 +2607,7 @@ async function createWorkspace() {
|
|
|
2557
2607
|
toast(t('ds.workspaceCreatedNoSession'), 'ok')
|
|
2558
2608
|
}
|
|
2559
2609
|
} catch (e) {
|
|
2610
|
+
if (!connection.valid()) return
|
|
2560
2611
|
toast(`${t('ds.workspaceCreateFailed')}:${e.message || t('ds.feedbackNetworkError')}`, 'err')
|
|
2561
2612
|
} finally {
|
|
2562
2613
|
createWorkspace.busy = false
|
|
@@ -2564,6 +2615,8 @@ async function createWorkspace() {
|
|
|
2564
2615
|
}
|
|
2565
2616
|
}
|
|
2566
2617
|
async function loadFs(dir, silent) {
|
|
2618
|
+
const connection = captureConnection(), request = ++filesRequest
|
|
2619
|
+
const valid = () => connection.valid() && request === filesRequest
|
|
2567
2620
|
if (!state.token) {
|
|
2568
2621
|
$('fs-path').textContent = t('ds.toastAuth')
|
|
2569
2622
|
$('fs-list').innerHTML = `<div class="ds-empty">${t('ds.toastAuth')}</div>`
|
|
@@ -2576,8 +2629,10 @@ async function loadFs(dir, silent) {
|
|
|
2576
2629
|
}
|
|
2577
2630
|
try {
|
|
2578
2631
|
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
2632
|
+
if (!valid()) return
|
|
2579
2633
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2580
2634
|
const data = await res.json().catch(() => ({}))
|
|
2635
|
+
if (!valid()) return
|
|
2581
2636
|
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
2582
2637
|
state.fs.path = data.path
|
|
2583
2638
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
@@ -2599,6 +2654,7 @@ async function loadFs(dir, silent) {
|
|
|
2599
2654
|
else window.open(fsApiUrl('/file', { path: row.dataset.fsPath, token: state.token }), '_blank')
|
|
2600
2655
|
}))
|
|
2601
2656
|
} catch (e) {
|
|
2657
|
+
if (!valid()) return
|
|
2602
2658
|
$('fs-path').textContent = target || '~'
|
|
2603
2659
|
$('fs-list').innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
2604
2660
|
}
|
|
@@ -2740,17 +2796,22 @@ function commitWorkspaceSessionOrder(workspaceId, order) {
|
|
|
2740
2796
|
toast(t('ds.wbOrderSaved'), 'ok')
|
|
2741
2797
|
}
|
|
2742
2798
|
async function refreshWorkbench({ silent = false } = {}) {
|
|
2799
|
+
const connection = captureConnection(), request = ++workbenchRequest
|
|
2800
|
+
const valid = () => connection.valid() && request === workbenchRequest
|
|
2743
2801
|
if (!state.token) { renderWorkbench(); return }
|
|
2744
2802
|
let wb = null
|
|
2745
2803
|
try {
|
|
2746
2804
|
wb = await wbGateway('GET', '/workbench')
|
|
2805
|
+
if (!valid()) return
|
|
2747
2806
|
state.wb.apiMissing = false
|
|
2748
2807
|
} catch (e) {
|
|
2808
|
+
if (!valid()) return
|
|
2749
2809
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
2750
2810
|
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
2751
2811
|
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
2752
2812
|
}
|
|
2753
2813
|
const wl = await safeRpc('workspace.list', {}, '')
|
|
2814
|
+
if (!valid()) return
|
|
2754
2815
|
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
2755
2816
|
state.workspaces = wl && Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2756
2817
|
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
@@ -2767,8 +2828,10 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2767
2828
|
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2768
2829
|
try {
|
|
2769
2830
|
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
2831
|
+
if (!valid()) return
|
|
2770
2832
|
if (listRes.ok) {
|
|
2771
2833
|
const listData = await listRes.json().catch(() => ({}))
|
|
2834
|
+
if (!valid()) return
|
|
2772
2835
|
if (Array.isArray(listData.entries)) {
|
|
2773
2836
|
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
2774
2837
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
@@ -2781,16 +2844,19 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2781
2844
|
if (have.has(wbPathKey(projectPath))) continue
|
|
2782
2845
|
try {
|
|
2783
2846
|
const created = await rpc('workspace.create', { path: projectPath })
|
|
2847
|
+
if (!valid()) return
|
|
2784
2848
|
if (created?.workspace) {
|
|
2785
2849
|
items.push(created.workspace)
|
|
2786
2850
|
state.workspaces.push(created.workspace)
|
|
2787
2851
|
have.add(wbPathKey(projectPath))
|
|
2788
2852
|
}
|
|
2789
2853
|
} catch {}
|
|
2854
|
+
if (!valid()) return
|
|
2790
2855
|
}
|
|
2791
2856
|
}
|
|
2792
2857
|
}
|
|
2793
2858
|
} catch {}
|
|
2859
|
+
if (!valid()) return
|
|
2794
2860
|
state.wb.projects = orderedWorkspaceItems(items
|
|
2795
2861
|
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
2796
2862
|
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true })))
|
|
@@ -3367,6 +3433,7 @@ function bindUi() {
|
|
|
3367
3433
|
if (!e.target.closest('#model-menu') && !e.target.closest('#btn-model')) $('model-menu')?.classList.add('hidden')
|
|
3368
3434
|
})
|
|
3369
3435
|
$('btn-stats-top').addEventListener('click', toggleStatsDrawer)
|
|
3436
|
+
$('btn-insights').addEventListener('click', () => openInsights())
|
|
3370
3437
|
$('stats-drawer-close').addEventListener('click', toggleStatsDrawer)
|
|
3371
3438
|
// 赞赏支持
|
|
3372
3439
|
$('btn-donate').addEventListener('click', openDonateModal)
|
|
@@ -3521,11 +3588,12 @@ start()
|
|
|
3521
3588
|
|
|
3522
3589
|
// Capture the connection so switching servers cannot redirect a plugin mutation.
|
|
3523
3590
|
document.getElementById('btn-plugin-center')?.addEventListener('click', () => {
|
|
3524
|
-
const
|
|
3525
|
-
const
|
|
3591
|
+
const connection = captureConnection()
|
|
3592
|
+
const server = connection.server || ''
|
|
3593
|
+
const token = connection.token
|
|
3526
3594
|
window.DshPluginCenter.open({
|
|
3527
3595
|
url: path => server + path,
|
|
3528
3596
|
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
3529
|
-
valid:
|
|
3597
|
+
valid: connection.valid,
|
|
3530
3598
|
})
|
|
3531
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)}
|