dsh-remote-plugin 0.6.13 → 0.6.14
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 +59 -4
- package/package.json +1 -1
- package/public/announcements.json +25 -0
- package/public/app.js +313 -31
- package/public/desktop/desktop.css +20 -0
- package/public/desktop/desktop.html +8 -4
- package/public/desktop/desktop.js +124 -24
- package/public/index.html +32 -12
- package/public/md.js +71 -1
- package/public/styles.css +27 -0
- package/public/transcribe-core.js +74 -0
- package/public/update.json +8 -8
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -13,14 +13,16 @@ const LS = {
|
|
|
13
13
|
}
|
|
14
14
|
const CLIENT_ID = (() => {
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
const key = 'dshRemoteClientIdV2'
|
|
17
|
+
let id = localStorage.getItem(key)
|
|
17
18
|
if (!id) {
|
|
18
19
|
id = (globalThis.crypto?.randomUUID?.() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`)
|
|
19
|
-
|
|
20
|
+
localStorage.setItem(key, id)
|
|
20
21
|
}
|
|
21
22
|
return id
|
|
22
23
|
} catch { return '' }
|
|
23
24
|
})()
|
|
25
|
+
function clientIdHeaders() { return CLIENT_ID ? { 'x-dsh-remote-client-id': CLIENT_ID } : {} }
|
|
24
26
|
|
|
25
27
|
/* 离线缓存: 会话列表 + 每会话聊天记录。只在网络失败时兜底展示, 不会替代线上数据。 */
|
|
26
28
|
const CACHE = {
|
|
@@ -71,6 +73,8 @@ const state = {
|
|
|
71
73
|
approvals: [], // 待处理审批
|
|
72
74
|
questions: [], // 待处理提问
|
|
73
75
|
queues: {}, // sessionId -> queue items
|
|
76
|
+
queueSteering: {}, // sessionId:itemId -> pending steer request
|
|
77
|
+
sessionTurnTimes: {}, // sessionId -> 本轮开始/结束时间,避免中间事件推动排序
|
|
74
78
|
jobs: {}, // sessionId -> jobs
|
|
75
79
|
history: emptyHistory(),
|
|
76
80
|
errCount: 0,
|
|
@@ -88,7 +92,8 @@ const state = {
|
|
|
88
92
|
wbProjects: [],
|
|
89
93
|
wbArchived: [],
|
|
90
94
|
wbOpen: false,
|
|
91
|
-
wbOpenProjects: {}
|
|
95
|
+
wbOpenProjects: {},
|
|
96
|
+
subagentExpandedSession: ''
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
const $ = (id) => document.getElementById(id)
|
|
@@ -363,6 +368,7 @@ async function getWsTicket() {
|
|
|
363
368
|
headers: {
|
|
364
369
|
authorization: 'Bearer ' + token,
|
|
365
370
|
'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web',
|
|
371
|
+
...clientIdHeaders(),
|
|
366
372
|
}
|
|
367
373
|
})
|
|
368
374
|
if (!res.ok) throw new Error('ws ticket HTTP ' + res.status)
|
|
@@ -414,7 +420,7 @@ async function loadStats() {
|
|
|
414
420
|
}
|
|
415
421
|
try {
|
|
416
422
|
const res = await fetch(apiUrl('/stats/summary?days=7'), {
|
|
417
|
-
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' }
|
|
423
|
+
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
418
424
|
})
|
|
419
425
|
if (res.status === 401) { authFailure(); return }
|
|
420
426
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
@@ -480,7 +486,7 @@ function renderStats(days) {
|
|
|
480
486
|
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
481
487
|
const opts = {
|
|
482
488
|
method: 'POST',
|
|
483
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
489
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() },
|
|
484
490
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
485
491
|
}
|
|
486
492
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -501,7 +507,7 @@ async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
|
501
507
|
async function respond(rpcId, value) {
|
|
502
508
|
const opts = {
|
|
503
509
|
method: 'POST',
|
|
504
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
510
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() },
|
|
505
511
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
506
512
|
}
|
|
507
513
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -1163,7 +1169,7 @@ async function pollKind(kind) {
|
|
|
1163
1169
|
let res
|
|
1164
1170
|
try {
|
|
1165
1171
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(5000) : undefined
|
|
1166
|
-
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' }
|
|
1172
|
+
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
1167
1173
|
res = signal ? await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}`), { signal, headers }) : await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}`), { headers })
|
|
1168
1174
|
} catch { return }
|
|
1169
1175
|
if (res.status === 401) { authFailure(); return }
|
|
@@ -1295,7 +1301,10 @@ function onHostFrame(full) {
|
|
|
1295
1301
|
function onSessionEvent(sessionId, event) {
|
|
1296
1302
|
if (!event) return
|
|
1297
1303
|
const s = state.byId.get(sessionId)
|
|
1298
|
-
if (
|
|
1304
|
+
if (event.type === 'turn/start' || event.type === 'turn/end') {
|
|
1305
|
+
noteSessionTurnTime(sessionId, event)
|
|
1306
|
+
renderSessions()
|
|
1307
|
+
}
|
|
1299
1308
|
if (event.type === 'agent/status') {
|
|
1300
1309
|
if (s) { s.running = !!event.data?.running; s.blank = false; if (s.running) s.error = false }
|
|
1301
1310
|
if (state.current === sessionId) { updateCancelBtn(); renderSessionSub(); updateSessionStatus() }
|
|
@@ -1469,7 +1478,7 @@ async function refreshWorkbench() {
|
|
|
1469
1478
|
if (!state.token) return
|
|
1470
1479
|
try {
|
|
1471
1480
|
const res = await fetch(apiUrl('/workbench'), {
|
|
1472
|
-
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' }
|
|
1481
|
+
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
1473
1482
|
})
|
|
1474
1483
|
if (res.ok) {
|
|
1475
1484
|
const value = await res.json().catch(() => null)
|
|
@@ -1546,7 +1555,7 @@ function renderWorkbench() {
|
|
|
1546
1555
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1547
1556
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
|
|
1548
1557
|
<span class="wb-session-title">${esc(titleOf(s))}</span>
|
|
1549
|
-
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(s
|
|
1558
|
+
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(sessionSortTime(s)))}</span>
|
|
1550
1559
|
</button>
|
|
1551
1560
|
<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
|
|
1552
1561
|
</div>`).join('') : `<div class="wb-empty">${esc(t('wb.noSessions'))}</div>`}</div>` : ''
|
|
@@ -1576,6 +1585,15 @@ function workspaceDisplayName(label) {
|
|
|
1576
1585
|
const parts = clean.split(/[\\/]/).filter(Boolean)
|
|
1577
1586
|
return parts[parts.length - 1] || value
|
|
1578
1587
|
}
|
|
1588
|
+
function sessionSortTime(s) {
|
|
1589
|
+
return Math.max(Number(state.sessionTurnTimes[s?.sessionId]) || 0, Number(s?.updatedAt) || 0, Number(s?.createdAt) || 0)
|
|
1590
|
+
}
|
|
1591
|
+
function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
1592
|
+
const raw = typeof eventOrTime === 'object' ? eventOrTime?.time : eventOrTime
|
|
1593
|
+
const time = Number(raw) > 0 ? Number(raw) : Date.now()
|
|
1594
|
+
if (!sessionId || !Number.isFinite(time)) return
|
|
1595
|
+
state.sessionTurnTimes[sessionId] = Math.max(Number(state.sessionTurnTimes[sessionId]) || 0, time)
|
|
1596
|
+
}
|
|
1579
1597
|
function sortedSessions() {
|
|
1580
1598
|
const items = topLevelSessions()
|
|
1581
1599
|
if (state.sessionSort === 'workspace') {
|
|
@@ -1583,10 +1601,10 @@ function sortedSessions() {
|
|
|
1583
1601
|
const aw = sessionCwd(a) || '\uffff'
|
|
1584
1602
|
const bw = sessionCwd(b) || '\uffff'
|
|
1585
1603
|
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1586
|
-
return byWorkspace || ((b
|
|
1604
|
+
return byWorkspace || (sessionSortTime(b) - sessionSortTime(a))
|
|
1587
1605
|
})
|
|
1588
1606
|
}
|
|
1589
|
-
return items.sort((a, b) => (b
|
|
1607
|
+
return items.sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
1590
1608
|
}
|
|
1591
1609
|
function renderSessions() {
|
|
1592
1610
|
const list = $('session-list')
|
|
@@ -1625,7 +1643,7 @@ function renderSessions() {
|
|
|
1625
1643
|
<div class="sc-title">${esc(title)}</div>
|
|
1626
1644
|
<div class="sc-meta">
|
|
1627
1645
|
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
1628
|
-
<span>${fmtTime(s
|
|
1646
|
+
<span>${fmtTime(sessionSortTime(s))}</span>
|
|
1629
1647
|
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1630
1648
|
${badge}${queueBadge}
|
|
1631
1649
|
</div>
|
|
@@ -1662,6 +1680,7 @@ async function openSession(id) {
|
|
|
1662
1680
|
$('session-cards').innerHTML = ''
|
|
1663
1681
|
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
1664
1682
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
1683
|
+
renderQueue()
|
|
1665
1684
|
restoreCachedHistory()
|
|
1666
1685
|
await loadHistory(true)
|
|
1667
1686
|
renderSessionCards()
|
|
@@ -1686,7 +1705,7 @@ function bindNativeBack() {
|
|
|
1686
1705
|
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1687
1706
|
if (customSelectCurrent) { closeCustomSelect(); return }
|
|
1688
1707
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1689
|
-
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else if (openModal.id === 'modal-app-version-warning') closeAppVersionWarning(false); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1708
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else if (openModal.id === 'modal-app-version-warning') closeAppVersionWarning(false); else if (openModal.id === 'modal-scan-live') closeLiveScan(''); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1690
1709
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1691
1710
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1692
1711
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1717,6 +1736,8 @@ function updateSessionStatus() {
|
|
|
1717
1736
|
const s = state.byId.get(state.current)
|
|
1718
1737
|
const head = $('session-head')
|
|
1719
1738
|
if (!head) return
|
|
1739
|
+
const composerStatus = $('composer-status')
|
|
1740
|
+
if (composerStatus) composerStatus.classList.toggle('hidden', !s?.running)
|
|
1720
1741
|
head.classList.remove('running', 'interrupted')
|
|
1721
1742
|
const queued = (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
1722
1743
|
if (s?.running || queued) head.classList.add('running')
|
|
@@ -1896,9 +1917,10 @@ async function loadHistory(reset) {
|
|
|
1896
1917
|
for (const entry of incoming) {
|
|
1897
1918
|
const ev = entry?.event
|
|
1898
1919
|
const seq = ev?.seq
|
|
1920
|
+
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
1899
1921
|
applyReasoningStreamEvent(ev)
|
|
1900
1922
|
if (seq == null || state.history.seqs.has(seq)) continue
|
|
1901
|
-
if (!shouldShowEvent(ev.type)) continue
|
|
1923
|
+
if (!shouldShowEvent(ev.type, ev)) continue // chunk 与非用户上下文不保留
|
|
1902
1924
|
state.history.seqs.add(seq)
|
|
1903
1925
|
state.history.visible.push({ seq, event: ev, view: entry.view })
|
|
1904
1926
|
added++
|
|
@@ -1929,7 +1951,7 @@ function insertLiveEvent(event) {
|
|
|
1929
1951
|
return
|
|
1930
1952
|
}
|
|
1931
1953
|
const seq = event?.seq
|
|
1932
|
-
if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type)) {
|
|
1954
|
+
if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type, event)) {
|
|
1933
1955
|
if (reasoningChanged) scheduleReasoningRender()
|
|
1934
1956
|
return
|
|
1935
1957
|
}
|
|
@@ -2069,9 +2091,24 @@ const INTERESTING_EVENTS = new Set([
|
|
|
2069
2091
|
'approval/asked', 'approval/resolved',
|
|
2070
2092
|
'session/title', 'title'
|
|
2071
2093
|
])
|
|
2072
|
-
function
|
|
2073
|
-
|
|
2074
|
-
return
|
|
2094
|
+
function messageSource(data) {
|
|
2095
|
+
const source = data?.source ?? data?.message?.source
|
|
2096
|
+
return source && typeof source === 'object' ? source : null
|
|
2097
|
+
}
|
|
2098
|
+
function isHumanUserMessage(event) {
|
|
2099
|
+
if (event?.type !== 'user/message') return false
|
|
2100
|
+
const source = messageSource(event.data || {})
|
|
2101
|
+
// Older DSH events may not carry source metadata; keep those visible for compatibility.
|
|
2102
|
+
return !source || source.kind === 'user'
|
|
2103
|
+
}
|
|
2104
|
+
function shouldShowEvent(type, event) {
|
|
2105
|
+
if (!INTERESTING_EVENTS.has(type)) return false
|
|
2106
|
+
if (type === 'user/message' && !isHumanUserMessage(event)) {
|
|
2107
|
+
const data = event?.data || {}
|
|
2108
|
+
const blocks = data.message?.content || data.content || []
|
|
2109
|
+
return systemReminderText(blocks).length > 0
|
|
2110
|
+
}
|
|
2111
|
+
return true
|
|
2075
2112
|
}
|
|
2076
2113
|
function systemReminderText(blocks) {
|
|
2077
2114
|
if (!Array.isArray(blocks)) return ''
|
|
@@ -2085,7 +2122,7 @@ function eventHtml(entry, ctx = {}) {
|
|
|
2085
2122
|
const ev = entry.event || {}
|
|
2086
2123
|
const data = ev.data || {}
|
|
2087
2124
|
const type = ev.type || 'event'
|
|
2088
|
-
if (!shouldShowEvent(type)) return ''
|
|
2125
|
+
if (!shouldShowEvent(type, ev)) return ''
|
|
2089
2126
|
let inner = ''
|
|
2090
2127
|
|
|
2091
2128
|
if (type === 'user/message' || type === 'assistant/message') {
|
|
@@ -2095,6 +2132,8 @@ function eventHtml(entry, ctx = {}) {
|
|
|
2095
2132
|
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
2096
2133
|
if (sysText) {
|
|
2097
2134
|
inner = `<details class="event event-detail" data-seq="${seq}"><summary>${esc(t('event.systemReminder'))}</summary><pre>${esc(truncate(sysText, 4000))}</pre></details>`
|
|
2135
|
+
} else if (type === 'user/message' && !isHumanUserMessage(ev)) {
|
|
2136
|
+
return ''
|
|
2098
2137
|
} else {
|
|
2099
2138
|
inner = `<div class="msg ${esc(role)}" data-seq="${seq}"><div class="role">${esc(role === 'user' ? t('role.me') : t('role.dsh'))}</div>${blocks.map(blockHtml).join('')}</div>`
|
|
2100
2139
|
}
|
|
@@ -2202,13 +2241,19 @@ async function renderSessionCards() {
|
|
|
2202
2241
|
const sub = await safeRpc('subagent.list', { parentSessionId: sessionId })
|
|
2203
2242
|
if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
|
|
2204
2243
|
if (sub?.entries?.length) {
|
|
2244
|
+
const expanded = state.subagentExpandedSession === sessionId
|
|
2245
|
+
const toggleLabel = expanded ? t('subagent.collapse') : t('subagent.expand')
|
|
2205
2246
|
const rows = sub.entries.map(e => {
|
|
2206
2247
|
if (e.kind === 'diagnostic') return `<div class="card-row"><span class="k">${t('subagent.diagnostic')}</span><span class="v">${esc(e.reason)}</span></div>`
|
|
2207
2248
|
const label = e.label || short(e.id)
|
|
2208
2249
|
const running = e.activity === 'running'
|
|
2209
2250
|
return `<div class="card-row"><span class="k">${running ? '▶ ' : ''}${esc(label)}</span><span class="v">${esc(e.mode)} ${running ? t('subagent.running') : ''}${e.mode === 'continuable' && running ? ` <button class="mini-btn" data-sub-interrupt="${esc(e.id)}">${t('subagent.interrupt')}</button>` : ''}</span></div>`
|
|
2210
2251
|
}).join('')
|
|
2211
|
-
box.insertAdjacentHTML('beforeend', `<div class="card"><
|
|
2252
|
+
box.insertAdjacentHTML('beforeend', `<div class="card subagent-card"><button type="button" class="subagent-toggle" data-subagent-toggle aria-expanded="${expanded}" aria-label="${esc(toggleLabel)}" title="${esc(toggleLabel)}"><span class="card-title">${esc(t('subagent.count', { n: sub.entries.length }))}</span><span class="subagent-toggle-icon" aria-hidden="true">${expanded ? '⌃' : '⌄'}</span></button><div class="subagent-list${expanded ? '' : ' hidden'}">${rows}</div></div>`)
|
|
2253
|
+
box.querySelector('[data-subagent-toggle]')?.addEventListener('click', () => {
|
|
2254
|
+
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
2255
|
+
renderSessionCards()
|
|
2256
|
+
})
|
|
2212
2257
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
2213
2258
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
2214
2259
|
}
|
|
@@ -2261,7 +2306,7 @@ async function runSlashCommand(text) {
|
|
|
2261
2306
|
: undefined
|
|
2262
2307
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
2263
2308
|
method: 'POST',
|
|
2264
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
2309
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() },
|
|
2265
2310
|
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
2266
2311
|
...(signal ? { signal } : {})
|
|
2267
2312
|
})
|
|
@@ -2388,7 +2433,12 @@ async function sendSessionContent(text, images) {
|
|
|
2388
2433
|
mode: 'queue',
|
|
2389
2434
|
content
|
|
2390
2435
|
}, t('send.failed'))
|
|
2391
|
-
if (v?.accepted) {
|
|
2436
|
+
if (v?.accepted) {
|
|
2437
|
+
noteSessionTurnTime(state.current, Date.now())
|
|
2438
|
+
renderSessions()
|
|
2439
|
+
toast(images.length ? t('send.imageSent') : (clean.startsWith('/') ? t('send.commandSent') : t('send.sent')), 'ok')
|
|
2440
|
+
return true
|
|
2441
|
+
}
|
|
2392
2442
|
if (v?.command?.text) { toast(t('send.commandExecuted'), 'ok'); return true }
|
|
2393
2443
|
return false
|
|
2394
2444
|
} catch (e) {
|
|
@@ -2735,7 +2785,7 @@ function renderOverview() {
|
|
|
2735
2785
|
|
|
2736
2786
|
const topSessions = topLevelSessions()
|
|
2737
2787
|
const running = topSessions.filter(s => s.running).length
|
|
2738
|
-
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (
|
|
2788
|
+
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (sessionSortTime(b) - sessionSortTime(a))).slice(0, 4)
|
|
2739
2789
|
const primary = $('overview-primary-action')
|
|
2740
2790
|
if (primary) {
|
|
2741
2791
|
let action = 'new'
|
|
@@ -2766,7 +2816,7 @@ function renderOverview() {
|
|
|
2766
2816
|
$('overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'overview.poll' : 'overview.ws') : '—'
|
|
2767
2817
|
$('overview-active-count').textContent = running ? t('overview.activeCount', { n: running }) : ''
|
|
2768
2818
|
$('overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="overview-session-item ${s.running ? 'running' : ''}" data-overview-session="${esc(s.sessionId)}">
|
|
2769
|
-
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(titleOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(s
|
|
2819
|
+
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(titleOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="overview-item-arrow">›</span>
|
|
2770
2820
|
</button>`).join('') : `<div class="overview-empty">${t('overview.noSession')}</div>`
|
|
2771
2821
|
$('overview-session-list').querySelectorAll('[data-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.overviewSession)))
|
|
2772
2822
|
}
|
|
@@ -2862,11 +2912,46 @@ async function submitQuestion() {
|
|
|
2862
2912
|
}
|
|
2863
2913
|
|
|
2864
2914
|
/* ---------------- 后台任务 ---------------- */
|
|
2915
|
+
function queuePreview(item) {
|
|
2916
|
+
const blocks = item?.message?.content || item?.content || []
|
|
2917
|
+
const text = Array.isArray(blocks)
|
|
2918
|
+
? blocks.filter(block => block?.type === 'text').map(block => String(block.text || '')).join(' ').trim()
|
|
2919
|
+
: ''
|
|
2920
|
+
return text || (Array.isArray(blocks) && blocks.some(block => block?.type === 'image') ? t('queue.image') : '…')
|
|
2921
|
+
}
|
|
2922
|
+
async function steerQueueItem(itemId) {
|
|
2923
|
+
const sessionId = state.current
|
|
2924
|
+
const key = `${sessionId}:${itemId}`
|
|
2925
|
+
const s = state.byId.get(sessionId)
|
|
2926
|
+
if (!sessionId || !s?.running || state.queueSteering[key]) return
|
|
2927
|
+
state.queueSteering[key] = true
|
|
2928
|
+
renderQueue()
|
|
2929
|
+
try {
|
|
2930
|
+
const v = await safeRpc('session.updateQueue', { sessionId, itemId, action: { kind: 'steer' } }, t('queue.steerFailed', { msg: '' }).replace(/:$/, '').replace(/: $/, ''))
|
|
2931
|
+
if (v?.accepted) toast(t('queue.steerSubmitted'), 'ok')
|
|
2932
|
+
} finally {
|
|
2933
|
+
delete state.queueSteering[key]
|
|
2934
|
+
renderQueue()
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2865
2937
|
function renderQueue() {
|
|
2866
2938
|
const s = state.byId.get(state.current)
|
|
2867
2939
|
if (!s) return
|
|
2868
|
-
const items = state.queues[state.current] || []
|
|
2940
|
+
const items = (state.queues[state.current] || []).filter(item => item?.placement === 'queued')
|
|
2941
|
+
const box = $('queue-dock')
|
|
2942
|
+
if (box) {
|
|
2943
|
+
box.classList.toggle('hidden', !items.length)
|
|
2944
|
+
box.innerHTML = items.length ? `<div class="queue-dock-head"><span>⌁</span><span>${esc(t('queue.title'))} · ${items.length}</span></div><div class="queue-dock-list">${items.map(item => {
|
|
2945
|
+
const key = `${state.current}:${item.id}`
|
|
2946
|
+
const busy = !!state.queueSteering[key]
|
|
2947
|
+
return `<div class="queue-dock-item"><span class="queue-dock-preview" title="${esc(queuePreview(item))}">${esc(queuePreview(item))}</span><button type="button" class="mini-btn queue-dock-action" data-queue-steer="${esc(item.id)}" title="${esc(s.running ? t('queue.steer') : t('queue.steerUnavailable'))}" ${s.running && !busy ? '' : 'disabled'}>${busy ? '…' : esc(t('queue.steer'))}</button></div>`
|
|
2948
|
+
}).join('')}</div>` : ''
|
|
2949
|
+
box.querySelectorAll('[data-queue-steer]').forEach(button => {
|
|
2950
|
+
button.addEventListener('click', () => steerQueueItem(button.dataset.queueSteer))
|
|
2951
|
+
})
|
|
2952
|
+
}
|
|
2869
2953
|
updateCancelBtn()
|
|
2954
|
+
updateSessionStatus()
|
|
2870
2955
|
// 队列数量在会话列表已显示; 详情页不重复大 UI
|
|
2871
2956
|
$('history-hint').textContent = items.length ? t('history.queueAndCount', { q: items.length, n: state.history.visible.length }) : t('history.countOnly', { n: state.history.visible.length })
|
|
2872
2957
|
renderSessions()
|
|
@@ -2889,7 +2974,8 @@ function renderJobs() {
|
|
|
2889
2974
|
function fsHeaders() {
|
|
2890
2975
|
return {
|
|
2891
2976
|
authorization: 'Bearer ' + state.token,
|
|
2892
|
-
'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web'
|
|
2977
|
+
'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web',
|
|
2978
|
+
...clientIdHeaders()
|
|
2893
2979
|
}
|
|
2894
2980
|
}
|
|
2895
2981
|
|
|
@@ -3259,6 +3345,7 @@ async function runFsUpload(up) {
|
|
|
3259
3345
|
xhr.open('POST', fsApiUrl('/upload', params))
|
|
3260
3346
|
xhr.setRequestHeader('authorization', 'Bearer ' + state.token)
|
|
3261
3347
|
xhr.setRequestHeader('x-dsh-remote-client', CAP?.isNativePlatform?.() ? 'app' : 'web')
|
|
3348
|
+
if (CLIENT_ID) xhr.setRequestHeader('x-dsh-remote-client-id', CLIENT_ID)
|
|
3262
3349
|
xhr.upload.onprogress = (e) => {
|
|
3263
3350
|
if (e.lengthComputable) {
|
|
3264
3351
|
const loaded = up.offset + Math.min(e.loaded, e.total)
|
|
@@ -4179,7 +4266,7 @@ function saveBgConfig(enabled) {
|
|
|
4179
4266
|
const base = bgBase()
|
|
4180
4267
|
const intervalMin = parseFloat($('bg-interval')?.value || '1') || 1
|
|
4181
4268
|
const notifyTaskDone = $('opt-task-done')?.checked !== false
|
|
4182
|
-
b.saveBackgroundConfig(JSON.stringify({ enabled, intervalMin, base, token: state.token || '', notifyTaskDone }))
|
|
4269
|
+
b.saveBackgroundConfig(JSON.stringify({ enabled, intervalMin, base, token: state.token || '', clientId: CLIENT_ID || '', notifyTaskDone }))
|
|
4183
4270
|
if (enabled) $('bg-auth-status')?.classList.add('hidden')
|
|
4184
4271
|
return true
|
|
4185
4272
|
}
|
|
@@ -4505,14 +4592,207 @@ async function decodeQrDataUrl(dataUrl) {
|
|
|
4505
4592
|
return code?.data || ''
|
|
4506
4593
|
}
|
|
4507
4594
|
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4595
|
+
let liveScanStream = null
|
|
4596
|
+
let liveScanTimer = null
|
|
4597
|
+
let liveScanResolve = null
|
|
4598
|
+
let liveScanCanvas = null
|
|
4599
|
+
let liveScanContext = null
|
|
4600
|
+
let liveScanCancelled = false
|
|
4601
|
+
let liveScanDetector = null
|
|
4602
|
+
let liveScanBusy = false
|
|
4603
|
+
let liveScanWorker = null
|
|
4604
|
+
let liveScanWorkerUrl = ''
|
|
4605
|
+
let liveScanWorkerResolve = null
|
|
4606
|
+
|
|
4607
|
+
function stopLiveScanWorker() {
|
|
4608
|
+
liveScanWorkerResolve?.('')
|
|
4609
|
+
liveScanWorkerResolve = null
|
|
4610
|
+
liveScanWorker?.terminate()
|
|
4611
|
+
liveScanWorker = null
|
|
4612
|
+
if (liveScanWorkerUrl) URL.revokeObjectURL(liveScanWorkerUrl)
|
|
4613
|
+
liveScanWorkerUrl = ''
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
function startLiveScanWorker() {
|
|
4617
|
+
if (!window.Worker || !window.Blob || !window.URL?.createObjectURL) return
|
|
4618
|
+
try {
|
|
4619
|
+
const sourceUrl = new URL('jsqr.min.js', document.baseURI).href
|
|
4620
|
+
const workerSource = `
|
|
4621
|
+
let loaded = false
|
|
4622
|
+
self.onmessage = event => {
|
|
4623
|
+
try {
|
|
4624
|
+
if (!loaded) {
|
|
4625
|
+
self.importScripts(${JSON.stringify(sourceUrl)})
|
|
4626
|
+
loaded = true
|
|
4627
|
+
}
|
|
4628
|
+
const { data, width, height } = event.data || {}
|
|
4629
|
+
const code = self.jsQR?.(data, width, height, { inversionAttempts: 'attemptBoth' })
|
|
4630
|
+
self.postMessage({ raw: code?.data || '' })
|
|
4631
|
+
} catch (error) {
|
|
4632
|
+
self.postMessage({ raw: '', error: String(error?.message || error) })
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
`
|
|
4636
|
+
liveScanWorkerUrl = URL.createObjectURL(new Blob([workerSource], { type: 'application/javascript' }))
|
|
4637
|
+
liveScanWorker = new Worker(liveScanWorkerUrl)
|
|
4638
|
+
liveScanWorker.onmessage = event => {
|
|
4639
|
+
const resolve = liveScanWorkerResolve
|
|
4640
|
+
liveScanWorkerResolve = null
|
|
4641
|
+
resolve?.(event.data?.raw || '')
|
|
4642
|
+
}
|
|
4643
|
+
liveScanWorker.onerror = () => {
|
|
4644
|
+
const resolve = liveScanWorkerResolve
|
|
4645
|
+
liveScanWorkerResolve = null
|
|
4646
|
+
resolve?.('')
|
|
4647
|
+
stopLiveScanWorker()
|
|
4648
|
+
}
|
|
4649
|
+
} catch {
|
|
4650
|
+
stopLiveScanWorker()
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
function closeLiveScan(result = '') {
|
|
4655
|
+
liveScanCancelled = true
|
|
4656
|
+
if (liveScanTimer) clearTimeout(liveScanTimer)
|
|
4657
|
+
liveScanTimer = null
|
|
4658
|
+
if (liveScanStream) liveScanStream.getTracks().forEach(track => track.stop())
|
|
4659
|
+
liveScanStream = null
|
|
4660
|
+
liveScanDetector = null
|
|
4661
|
+
stopLiveScanWorker()
|
|
4662
|
+
const video = $('scan-live-video')
|
|
4663
|
+
if (video) video.srcObject = null
|
|
4664
|
+
$('modal-scan-live')?.classList.add('hidden')
|
|
4665
|
+
const resolve = liveScanResolve
|
|
4666
|
+
liveScanResolve = null
|
|
4667
|
+
resolve?.(result)
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
async function scanLiveFrame() {
|
|
4671
|
+
if (!liveScanResolve) return
|
|
4672
|
+
const video = $('scan-live-video')
|
|
4673
|
+
if (!video || video.readyState < 2 || !video.videoWidth || !liveScanContext) {
|
|
4674
|
+
liveScanTimer = setTimeout(scanLiveFrame, 180)
|
|
4675
|
+
return
|
|
4676
|
+
}
|
|
4677
|
+
if (liveScanBusy) return
|
|
4678
|
+
liveScanBusy = true
|
|
4679
|
+
try {
|
|
4680
|
+
let raw = ''
|
|
4681
|
+
if (liveScanDetector) {
|
|
4682
|
+
const codes = await liveScanDetector.detect(video)
|
|
4683
|
+
raw = codes?.[0]?.rawValue || ''
|
|
4684
|
+
} else {
|
|
4685
|
+
// 只解码取景框中央区域,避免在低端手机上对整幅高分辨率画面反复二值化。
|
|
4686
|
+
const side = Math.floor(Math.min(video.videoWidth, video.videoHeight) * 0.64)
|
|
4687
|
+
const sx = Math.floor((video.videoWidth - side) / 2)
|
|
4688
|
+
const sy = Math.floor((video.videoHeight - side) / 2)
|
|
4689
|
+
const maxSide = 480
|
|
4690
|
+
const scale = Math.min(1, maxSide / Math.max(1, side))
|
|
4691
|
+
const w = Math.max(1, Math.round(side * scale))
|
|
4692
|
+
const h = w
|
|
4693
|
+
if (liveScanCanvas.width !== w || liveScanCanvas.height !== h) {
|
|
4694
|
+
liveScanCanvas.width = w
|
|
4695
|
+
liveScanCanvas.height = h
|
|
4696
|
+
}
|
|
4697
|
+
liveScanContext.drawImage(video, sx, sy, side, side, 0, 0, w, h)
|
|
4698
|
+
const imageData = liveScanContext.getImageData(0, 0, w, h)
|
|
4699
|
+
if (liveScanWorker) {
|
|
4700
|
+
raw = await new Promise(resolve => {
|
|
4701
|
+
liveScanWorkerResolve = resolve
|
|
4702
|
+
liveScanWorker.postMessage({ data: imageData.data, width: w, height: h }, [imageData.data.buffer])
|
|
4703
|
+
})
|
|
4704
|
+
} else {
|
|
4705
|
+
raw = window.jsQR?.(imageData.data, w, h, { inversionAttempts: 'attemptBoth' })?.data || ''
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (raw && liveScanResolve) return closeLiveScan(raw)
|
|
4709
|
+
} catch {
|
|
4710
|
+
// 摄像头帧在切后台或权限切换时可能暂时不可读,下一帧继续即可。
|
|
4711
|
+
} finally {
|
|
4712
|
+
liveScanBusy = false
|
|
4713
|
+
}
|
|
4714
|
+
if (liveScanResolve) liveScanTimer = setTimeout(scanLiveFrame, 180)
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
/** 打开持续取帧的本地摄像头扫码;返回 undefined 表示当前 WebView 不支持实时摄像头。 */
|
|
4718
|
+
async function scanPairLive() {
|
|
4719
|
+
if (!navigator.mediaDevices?.getUserMedia) return undefined
|
|
4720
|
+
const video = $('scan-live-video')
|
|
4721
|
+
const modal = $('modal-scan-live')
|
|
4722
|
+
if (!video || !modal || !window.jsQR) return undefined
|
|
4723
|
+
const camera = CAP.Plugins?.Camera
|
|
4724
|
+
const perm = await camera?.requestPermissions?.({ permissions: ['camera'] })
|
|
4725
|
+
if (perm && perm.camera !== 'granted') throw new Error(t('scan.permissionDenied'))
|
|
4726
|
+
liveScanCancelled = false
|
|
4727
|
+
modal.classList.remove('hidden')
|
|
4728
|
+
$('scan-live-status').textContent = t('scan.liveStarting')
|
|
4729
|
+
try {
|
|
4730
|
+
liveScanStream = await navigator.mediaDevices.getUserMedia({
|
|
4731
|
+
audio: false,
|
|
4732
|
+
video: {
|
|
4733
|
+
facingMode: { ideal: 'environment' },
|
|
4734
|
+
width: { ideal: 960, max: 1280 },
|
|
4735
|
+
height: { ideal: 720, max: 1280 },
|
|
4736
|
+
frameRate: { ideal: 24, max: 30 }
|
|
4737
|
+
}
|
|
4738
|
+
})
|
|
4739
|
+
if (liveScanCancelled) {
|
|
4740
|
+
liveScanStream.getTracks().forEach(track => track.stop())
|
|
4741
|
+
liveScanStream = null
|
|
4742
|
+
return ''
|
|
4743
|
+
}
|
|
4744
|
+
video.srcObject = liveScanStream
|
|
4745
|
+
await video.play()
|
|
4746
|
+
$('scan-live-status').textContent = t('scan.liveHint')
|
|
4747
|
+
try {
|
|
4748
|
+
if (window.BarcodeDetector) {
|
|
4749
|
+
const formats = await window.BarcodeDetector.getSupportedFormats?.()
|
|
4750
|
+
if (!formats || formats.includes('qr_code')) liveScanDetector = new window.BarcodeDetector({ formats: ['qr_code'] })
|
|
4751
|
+
}
|
|
4752
|
+
} catch { liveScanDetector = null }
|
|
4753
|
+
liveScanCanvas = document.createElement('canvas')
|
|
4754
|
+
liveScanContext = liveScanCanvas.getContext('2d', { willReadFrequently: true })
|
|
4755
|
+
if (!liveScanContext) throw new Error(t('scan.decodeUnsupported'))
|
|
4756
|
+
startLiveScanWorker()
|
|
4757
|
+
return await new Promise(resolve => {
|
|
4758
|
+
liveScanResolve = resolve
|
|
4759
|
+
scanLiveFrame()
|
|
4760
|
+
})
|
|
4761
|
+
} catch (e) {
|
|
4762
|
+
closeLiveScan('')
|
|
4763
|
+
throw e
|
|
4764
|
+
} finally {
|
|
4765
|
+
liveScanCanvas = null
|
|
4766
|
+
liveScanContext = null
|
|
4767
|
+
liveScanDetector = null
|
|
4768
|
+
liveScanBusy = false
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
/** App 内扫码优先使用实时摄像头取帧;不支持时回退到官方 Camera 拍照/相册 + jsQR。 */
|
|
4511
4773
|
async function scanPair(source) {
|
|
4512
4774
|
if (!CAP?.isNativePlatform?.()) {
|
|
4513
4775
|
toast(t('scan.browserHint'), 'err')
|
|
4514
4776
|
return
|
|
4515
4777
|
}
|
|
4778
|
+
if (source === 'CAMERA') {
|
|
4779
|
+
try {
|
|
4780
|
+
const liveRaw = await scanPairLive()
|
|
4781
|
+
if (liveRaw !== undefined) {
|
|
4782
|
+
if (!liveRaw) return toast(t('scan.cancelled'), 'ok')
|
|
4783
|
+
if (applyPairUrl(liveRaw)) {
|
|
4784
|
+
toast(t('scan.paired'), 'ok')
|
|
4785
|
+
openStreams()
|
|
4786
|
+
refreshAll()
|
|
4787
|
+
} else toast(t('scan.notPair'), 'err')
|
|
4788
|
+
return
|
|
4789
|
+
}
|
|
4790
|
+
} catch (e) {
|
|
4791
|
+
const msg = String(e?.message || e || '')
|
|
4792
|
+
toast(/cancel/i.test(msg) ? t('scan.cancelled') : t('scan.failed', { msg }), 'err')
|
|
4793
|
+
return
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4516
4796
|
const camera = CAP.Plugins?.Camera
|
|
4517
4797
|
if (!camera?.getPhoto) { toast(t('scan.unsupported'), 'err'); return }
|
|
4518
4798
|
try {
|
|
@@ -5121,6 +5401,8 @@ function bindUi() {
|
|
|
5121
5401
|
})
|
|
5122
5402
|
$('btn-scan-camera').addEventListener('click', () => scanPair('CAMERA'))
|
|
5123
5403
|
$('btn-scan-gallery').addEventListener('click', () => scanPair('PHOTOS'))
|
|
5404
|
+
$('scan-live-cancel')?.addEventListener('click', () => closeLiveScan(''))
|
|
5405
|
+
$('modal-scan-live')?.addEventListener('click', e => { if (e.target === $('modal-scan-live')) closeLiveScan('') })
|
|
5124
5406
|
$('btn-change-token').addEventListener('click', () => {
|
|
5125
5407
|
const input = prompt(t('token.prompt'), state.token)
|
|
5126
5408
|
if (input && input.trim()) { state.token = input.trim(); LS.set('token', input.trim()); $('token-desc').textContent = t('token.saved'); toast(t('token.savedReconnect'), 'ok'); openStreams(); refreshAll(); syncBgConfig() }
|
|
@@ -236,6 +236,18 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
236
236
|
/* 会话视图 */
|
|
237
237
|
#view-chat { padding: 0; }
|
|
238
238
|
.ds-history { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 18px 22px; display: flex; flex-direction: column; gap: 10px; }
|
|
239
|
+
.ds-queue-dock { flex: none; margin: 0 16px 8px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-surface); overflow: hidden; }
|
|
240
|
+
.ds-queue-dock-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: var(--dsr-muted); font-size: 12px; font-weight: 700; border-bottom: 1px solid var(--dsr-line); }
|
|
241
|
+
.ds-queue-dock-list { max-height: 220px; overflow-y: auto; }
|
|
242
|
+
.ds-queue-dock-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px 8px 12px; border-bottom: 1px solid var(--dsr-line); }
|
|
243
|
+
.ds-queue-dock-item:last-child { border-bottom: 0; }
|
|
244
|
+
.ds-queue-dock-preview { flex: 1; min-width: 0; color: var(--dsr-text); font-size: 13px; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
245
|
+
.ds-queue-dock-action { flex: 0 0 auto; }
|
|
246
|
+
.md-table-wrap { max-width: 100%; overflow-x: auto; margin: 8px 0; }
|
|
247
|
+
.md-table-wrap table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: .94em; }
|
|
248
|
+
.md-table-wrap th, .md-table-wrap td { padding: 6px 9px; border: 1px solid var(--dsr-line); text-align: left; white-space: nowrap; }
|
|
249
|
+
.md-table-wrap th { background: var(--dsr-bg-2); font-weight: 700; }
|
|
250
|
+
.md-table-wrap tbody tr:nth-child(even) { background: color-mix(in srgb, var(--dsr-bg-2) 45%, transparent); }
|
|
239
251
|
.ds-msg { max-width: min(78%, 680px); width: fit-content; box-sizing: border-box; padding: 9px 12px; border-radius: 12px; font-size: 13.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
240
252
|
.ds-msg.user { align-self: flex-end; background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line); color: var(--dsr-accent-strong); }
|
|
241
253
|
.ds-msg.assistant { align-self: flex-start; background: var(--dsr-panel); border: 1px solid var(--dsr-line); color: var(--dsr-text); }
|
|
@@ -274,6 +286,11 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
274
286
|
.ds-card-row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; padding: 3px 0; }
|
|
275
287
|
.ds-card-k { flex: 1; min-width: 0; word-break: break-word; }
|
|
276
288
|
.ds-card-v { color: var(--dsr-muted); font-size: 11.5px; white-space: nowrap; }
|
|
289
|
+
.ds-subagent-card { padding: 0; overflow: hidden; }
|
|
290
|
+
.ds-subagent-toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer; }
|
|
291
|
+
.ds-subagent-toggle .ds-card-title { margin: 0; }
|
|
292
|
+
.ds-subagent-toggle-icon { color: var(--dsr-muted); font-size: 16px; line-height: 1; }
|
|
293
|
+
.ds-subagent-list { padding: 0 12px 10px; border-top: 1px solid var(--dsr-line); }
|
|
277
294
|
.ds-model-head { font-size: 11px; font-weight: 700; color: var(--dsr-muted); letter-spacing: .6px; padding: 4px 6px 2px; }
|
|
278
295
|
.ds-model-group { padding: 2px 0 4px; }
|
|
279
296
|
.ds-model-provider { font-size: 11px; color: var(--dsr-muted); padding: 2px 6px; }
|
|
@@ -289,6 +306,9 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
289
306
|
.ds-reasoning-live summary::after { content: ''; display: inline-block; width: 6px; height: 6px; margin-left: 7px; border-radius: 50%; background: var(--dsr-accent-strong); animation: ds-reasoning-pulse 1.2s ease-in-out infinite; }
|
|
290
307
|
@keyframes ds-reasoning-pulse { 0%, 100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
|
|
291
308
|
.ds-composer { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--dsr-line); background: var(--dsr-panel); }
|
|
309
|
+
.ds-composer-status { display: flex; align-items: center; gap: 7px; padding: 0 3px; color: var(--dsr-accent-strong); font-size: 12px; font-weight: 700; }
|
|
310
|
+
.ds-composer-status-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 0 currentColor; animation: dsr-running-pulse 1.3s ease-out infinite; }
|
|
311
|
+
@keyframes dsr-running-pulse { 0% { box-shadow: 0 0 0 0 currentColor; opacity: 1; } 70% { box-shadow: 0 0 0 6px transparent; opacity: .65; } 100% { box-shadow: 0 0 0 0 transparent; opacity: 1; } }
|
|
292
312
|
.ds-composer textarea { width: 100%; min-width: 0; resize: none; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; font: inherit; font-size: 13.5px; line-height: 1.5; outline: none; min-height: 44px; max-height: 120px; box-sizing: border-box; }
|
|
293
313
|
.ds-composer-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
294
314
|
.ds-composer-left { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
|