dsh-remote-plugin 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +322 -19
- package/index.mjs +41 -9
- package/insights.mjs +73 -0
- package/package.json +3 -2
- package/plugin-center.mjs +118 -21
- package/public/admin.html +3 -0
- package/public/admin.js +1 -0
- package/public/app.js +403 -470
- package/public/desktop/desktop.css +101 -0
- package/public/desktop/desktop.html +63 -20
- package/public/desktop/desktop.js +324 -99
- package/public/history.js +160 -0
- package/public/index.html +73 -84
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/link-check.js +109 -0
- package/public/plugin-center.css +100 -1
- package/public/plugin-center.js +222 -60
- package/public/plugin.js +7 -1
- package/public/styles.css +107 -95
- package/public/update.json +12 -12
- package/public/version.json +1 -1
|
@@ -52,7 +52,7 @@ themeApply()
|
|
|
52
52
|
|
|
53
53
|
/* ---------------- 状态 ---------------- */
|
|
54
54
|
function emptyDesktopHistory() {
|
|
55
|
-
return { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity, partialReasoning: new Map() }
|
|
55
|
+
return { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity, partialReasoning: new Map(), loaded:false, cached:false, tailEvicted:false }
|
|
56
56
|
}
|
|
57
57
|
const state = {
|
|
58
58
|
token: LS.get('token', ''),
|
|
@@ -102,6 +102,41 @@ 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.hostInfo = null
|
|
118
|
+
if(typeof closeStream==='function'&&typeof streamMeta!=='undefined'){closeStream('mux');closeStream('host')}
|
|
119
|
+
state.current = null; state.history = emptyDesktopHistory()
|
|
120
|
+
state.fs.path = null; state.fs.initial = null; state.fs.loaded = false; state.fs.roots = []; state.fs.rootIndex = 0
|
|
121
|
+
state.workspaces = []; state.archivedIds = []; state.wb = { bound: false, path: '', projects: null }
|
|
122
|
+
window.DshPluginCenter?.close()
|
|
123
|
+
window.DshInsights?.close()
|
|
124
|
+
if(typeof document!=='undefined') {
|
|
125
|
+
const identityGeneration=connectionGeneration
|
|
126
|
+
queueMicrotask(()=>{if(identityGeneration===connectionGeneration&&typeof startOverviewDetection==='function')startOverviewDetection(true,true)})
|
|
127
|
+
document.getElementById('history')?.replaceChildren?.()
|
|
128
|
+
const generation=connectionGeneration
|
|
129
|
+
queueMicrotask(()=>{if(generation!==connectionGeneration)return;if(typeof conversationView!=='undefined')conversationView?.reset();restoreCachedSessionList()})
|
|
130
|
+
}
|
|
131
|
+
if (typeof document !== 'undefined') document.getElementById('modal-workspace')?.classList.add('hidden')
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
function captureConnection() {
|
|
136
|
+
const generation = connectionGeneration, server = state.server, token = state.token
|
|
137
|
+
return { server, token, valid: () => generation === connectionGeneration && server === state.server && token === state.token }
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
const streams = {}
|
|
106
141
|
let pollTimer = null
|
|
107
142
|
let wsRetryTimer = null
|
|
@@ -276,7 +311,7 @@ function renderModelMenu() {
|
|
|
276
311
|
if (state.models.loading) { box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${t('models.loading')}</span>`; return }
|
|
277
312
|
const groups = state.models.groups || []
|
|
278
313
|
if (!groups.length) {
|
|
279
|
-
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${(state.models.failures || []).map(f => f.name + ' ' + t('models.unavailable')).join(';') || t('models.none')}</span>`
|
|
314
|
+
box.innerHTML = `<div class="ds-model-head">${t('menu.modelTitle')}</div><span>${(state.models.failures || []).map(f => esc(f.name) + ' ' + t('models.unavailable')).join(';') || t('models.none')}</span>`
|
|
280
315
|
return
|
|
281
316
|
}
|
|
282
317
|
const cur = state.models.current
|
|
@@ -388,6 +423,9 @@ function openFeedbackModal() {
|
|
|
388
423
|
$('fb-msg').value = ''
|
|
389
424
|
$('fb-contact').value = ''
|
|
390
425
|
$('fb-include-diagnostics').checked = false
|
|
426
|
+
$('fb-include-crashes').checked = false
|
|
427
|
+
$('fb-crash-details').open = false
|
|
428
|
+
void loadCrashPreview()
|
|
391
429
|
$('modal-feedback').classList.remove('hidden')
|
|
392
430
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
393
431
|
}
|
|
@@ -508,11 +546,22 @@ async function checkNotesOnStart() {
|
|
|
508
546
|
openNotesModal(info)
|
|
509
547
|
} catch {}
|
|
510
548
|
}
|
|
549
|
+
async function loadCrashPreview() {
|
|
550
|
+
const connection=captureConnection(),preview=$('fb-crash-preview')
|
|
551
|
+
preview.textContent=t('ds.feedbackCrashLoading')
|
|
552
|
+
try {
|
|
553
|
+
const response=await fetch(apiUrl('/crash-logs'),{headers:{authorization:'Bearer '+connection.token},signal:AbortSignal.timeout(5000),cache:'no-store'})
|
|
554
|
+
if(!response.ok)throw new Error('unavailable')
|
|
555
|
+
const data=await response.json()
|
|
556
|
+
if(connection.valid())preview.textContent=JSON.stringify(data.upload,null,2)
|
|
557
|
+
}catch{if(connection.valid())preview.textContent=t('ds.feedbackCrashUnavailable')}
|
|
558
|
+
}
|
|
511
559
|
async function submitFeedback() {
|
|
512
560
|
const type = document.querySelector('#fb-chips .ds-fb-chip.current')?.dataset.fbType || 'bug'
|
|
513
561
|
const message = $('fb-msg').value.trim()
|
|
514
562
|
const contact = $('fb-contact').value.trim()
|
|
515
563
|
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
564
|
+
const includeCrashLogs = $('fb-include-crashes').checked
|
|
516
565
|
if (!message) { toast(t('ds.feedbackEmpty'), 'err'); return }
|
|
517
566
|
if (message.length > 2000) { toast(t('ds.feedbackTooLong'), 'err'); return }
|
|
518
567
|
const btn = $('fb-submit')
|
|
@@ -521,7 +570,7 @@ async function submitFeedback() {
|
|
|
521
570
|
const res = await fetch(apiUrl('/feedback'), {
|
|
522
571
|
method: 'POST',
|
|
523
572
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
524
|
-
body: JSON.stringify({ type, message, contact, appVersion: '', includeDiagnostics })
|
|
573
|
+
body: JSON.stringify({ type, message, contact, appVersion: '', includeDiagnostics, includeCrashLogs })
|
|
525
574
|
})
|
|
526
575
|
let json = {}
|
|
527
576
|
try { json = await res.json() } catch {}
|
|
@@ -607,14 +656,17 @@ async function respond(rpcId, value) {
|
|
|
607
656
|
return receipt?.accepted === true
|
|
608
657
|
}
|
|
609
658
|
async function safeRpc(method, payload, errText) {
|
|
659
|
+
const connection = captureConnection()
|
|
610
660
|
try { return await rpc(method, payload) }
|
|
611
661
|
catch (e) {
|
|
662
|
+
if (!connection.valid()) return null
|
|
612
663
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return null }
|
|
613
664
|
toast(errText ? `${errText}:${e.message}` : e.message, 'err')
|
|
614
665
|
return null
|
|
615
666
|
}
|
|
616
667
|
}
|
|
617
668
|
let hostDescribePromise = null
|
|
669
|
+
let hostDescribeConnection = null
|
|
618
670
|
let hostDescribeRetryTimer = null
|
|
619
671
|
let hostDescribeFailures = 0
|
|
620
672
|
|
|
@@ -630,12 +682,13 @@ function scheduleHostDescribeRetryDesktop() {
|
|
|
630
682
|
|
|
631
683
|
async function refreshHostDescriptionDesktop({ notify = false } = {}) {
|
|
632
684
|
if (!state.token) return null
|
|
633
|
-
if (hostDescribePromise) return hostDescribePromise
|
|
634
|
-
const
|
|
635
|
-
|
|
685
|
+
if (hostDescribePromise && hostDescribeConnection?.valid()) return hostDescribePromise
|
|
686
|
+
const connection = captureConnection()
|
|
687
|
+
hostDescribeConnection = connection
|
|
688
|
+
const job = (async () => {
|
|
636
689
|
try {
|
|
637
690
|
const host = await rpc('host.describe', {}, 5000)
|
|
638
|
-
if (
|
|
691
|
+
if (!connection.valid()) return null
|
|
639
692
|
state.hostInfo = host
|
|
640
693
|
const health = activeGatewayHealth()
|
|
641
694
|
if (health) health.upstreamReachable = true
|
|
@@ -645,17 +698,17 @@ async function refreshHostDescriptionDesktop({ notify = false } = {}) {
|
|
|
645
698
|
renderOverviewDesktop()
|
|
646
699
|
return host
|
|
647
700
|
} catch (error) {
|
|
648
|
-
if (
|
|
701
|
+
if (!connection.valid()) return null
|
|
649
702
|
hostDescribeFailures++
|
|
650
703
|
scheduleHostDescribeRetryDesktop()
|
|
651
704
|
if (notify) toast(error.message === 'AUTH' ? t('ds.toastAuth') : error.message, 'err')
|
|
652
705
|
return null
|
|
653
706
|
} finally {
|
|
654
|
-
hostDescribePromise = null
|
|
655
|
-
if (server !== state.server) scheduleHostDescribeRetryDesktop()
|
|
707
|
+
if (hostDescribePromise === job) { hostDescribePromise = null; hostDescribeConnection = null }
|
|
656
708
|
}
|
|
657
709
|
})()
|
|
658
|
-
|
|
710
|
+
hostDescribePromise = job
|
|
711
|
+
return job
|
|
659
712
|
}
|
|
660
713
|
function uuid() {
|
|
661
714
|
try { return crypto.randomUUID() } catch { return 'id-' + Date.now() + '-' + Math.random().toString(36).slice(2) }
|
|
@@ -1164,12 +1217,12 @@ function openStreams() {
|
|
|
1164
1217
|
function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
1165
1218
|
if (!state.token) return
|
|
1166
1219
|
if (ticket === null) {
|
|
1167
|
-
const token = state.token
|
|
1220
|
+
const token = state.token, connection=captureConnection()
|
|
1168
1221
|
void getWsTicket().then((value) => {
|
|
1169
|
-
if (
|
|
1222
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, value)
|
|
1170
1223
|
}).catch(() => {
|
|
1171
1224
|
// 兼容旧网关/插件副本: ticket 接口不可用时临时回退旧 token 握手。
|
|
1172
|
-
if (
|
|
1225
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, '')
|
|
1173
1226
|
})
|
|
1174
1227
|
return
|
|
1175
1228
|
}
|
|
@@ -1350,7 +1403,7 @@ window.addEventListener('online', () => {
|
|
|
1350
1403
|
document.addEventListener('visibilitychange', () => {
|
|
1351
1404
|
if (document.visibilityState === 'visible' && state.token &&
|
|
1352
1405
|
(streams.mux?.readyState !== WebSocket.OPEN || streams.host?.readyState !== WebSocket.OPEN)) {
|
|
1353
|
-
|
|
1406
|
+
repairMissingStreams()
|
|
1354
1407
|
}
|
|
1355
1408
|
})
|
|
1356
1409
|
|
|
@@ -1512,6 +1565,7 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1512
1565
|
s.projections.asOfSeq = Math.max(currentSeq, seq || 0)
|
|
1513
1566
|
if (state.current === sessionId) {
|
|
1514
1567
|
renderSessions()
|
|
1568
|
+
window.DshInsights?.update()
|
|
1515
1569
|
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
1516
1570
|
}
|
|
1517
1571
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
@@ -1538,6 +1592,10 @@ function resyncAfterStreamOpen() {
|
|
|
1538
1592
|
void refreshSessions().then(() => resyncCurrentSession())
|
|
1539
1593
|
}
|
|
1540
1594
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1595
|
+
function openInsights(tab = 'context') {
|
|
1596
|
+
window.DshInsights?.open({ tab, connection: captureConnection(), url: apiUrl,
|
|
1597
|
+
getSession: () => state.byId.get(state.current), getTitle: () => titleOf(state.byId.get(state.current)) })
|
|
1598
|
+
}
|
|
1541
1599
|
function sessionTitleValue(s) {
|
|
1542
1600
|
const value = proj(s, 'title', '')
|
|
1543
1601
|
return value == null ? '' : String(value).trim()
|
|
@@ -1552,7 +1610,11 @@ function sessionLabelOf(s) {
|
|
|
1552
1610
|
function isTopLevelSession(session) {
|
|
1553
1611
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1554
1612
|
}
|
|
1555
|
-
|
|
1613
|
+
// DSH marks sessions created via "new session" but never prompted as blank and hides
|
|
1614
|
+
// them in its own UI; keep the same semantics here so they do not occupy the list,
|
|
1615
|
+
// the workspace tree or the home stats.
|
|
1616
|
+
function isVisibleSession(session) { return isTopLevelSession(session) && !session?.blank }
|
|
1617
|
+
function topLevelSessions() { return state.sessions.filter(isVisibleSession) }
|
|
1556
1618
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1557
1619
|
function isGoalTerminal(goal) {
|
|
1558
1620
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -1587,6 +1649,7 @@ function onSessionEvent(sessionId, event) {
|
|
|
1587
1649
|
renderSessions()
|
|
1588
1650
|
}
|
|
1589
1651
|
const session = state.byId.get(sessionId)
|
|
1652
|
+
if((event?.type==='session/title'||event?.type==='title')&&event.data?.title&&session){session.projections??={values:{}};session.projections.values??={};session.projections.values.title=event.data.title;if(state.current===sessionId)updateSessionActions()}
|
|
1590
1653
|
if (event?.type === 'agent/status' && session) {
|
|
1591
1654
|
session.running = !!event.data?.running
|
|
1592
1655
|
if (state.current === sessionId) { renderQueue(); updateComposerStatus() }
|
|
@@ -1603,7 +1666,9 @@ function onSessionEvent(sessionId, event) {
|
|
|
1603
1666
|
h.seqs.add(seq)
|
|
1604
1667
|
h.visible.push({ seq, event })
|
|
1605
1668
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
1669
|
+
trimVisible()
|
|
1606
1670
|
renderHistory()
|
|
1671
|
+
scheduleHistoryCacheSave()
|
|
1607
1672
|
}
|
|
1608
1673
|
if (String(event.type || '').startsWith('goal/') || String(event.type || '').startsWith('todo/')) renderSessionCards()
|
|
1609
1674
|
}
|
|
@@ -1611,11 +1676,15 @@ function onSessionEvent(sessionId, event) {
|
|
|
1611
1676
|
|
|
1612
1677
|
/* ---------------- 会话 ---------------- */
|
|
1613
1678
|
async function refreshSessions() {
|
|
1679
|
+
if(typeof restoreCachedSessionList==='function')restoreCachedSessionList()
|
|
1680
|
+
const connection = captureConnection(), request = ++sessionsRequest
|
|
1614
1681
|
const v = await safeRpc('session.list', {}, '')
|
|
1682
|
+
if (!connection.valid() || request !== sessionsRequest) return
|
|
1615
1683
|
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1616
1684
|
state.sessions = v.items || []
|
|
1617
1685
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1618
1686
|
applyPendingProjections()
|
|
1687
|
+
try{LS.set(historyCacheScope()+':sessions',JSON.stringify(state.sessions.slice(0,80)))}catch{}
|
|
1619
1688
|
renderSessions()
|
|
1620
1689
|
scheduleWorkbenchRefresh()
|
|
1621
1690
|
renderOverviewDesktop()
|
|
@@ -1736,9 +1805,9 @@ function renderSessions() {
|
|
|
1736
1805
|
}
|
|
1737
1806
|
|
|
1738
1807
|
const emptySessionCleanup = new Set()
|
|
1739
|
-
async function archiveEmptySessionOnLeave(sessionId) {
|
|
1740
|
-
const base = state.server
|
|
1741
|
-
const protectedSession = () => state.server !== base || state.current !== sessionId
|
|
1808
|
+
async function archiveEmptySessionOnLeave(sessionId, leaving = false) {
|
|
1809
|
+
const base = state.server, connection=captureConnection()
|
|
1810
|
+
const protectedSession = () => !connection.valid() || state.server !== base || (!leaving && state.current !== sessionId)
|
|
1742
1811
|
|| !state.byId.has(sessionId) || state.byId.get(sessionId)?.running
|
|
1743
1812
|
|| state.sessionActivity?.has(sessionId) || state.pendingPrompts?.has(sessionId)
|
|
1744
1813
|
|| (state.queues[sessionId] || []).length > 0
|
|
@@ -1759,14 +1828,16 @@ async function archiveEmptySessionOnLeave(sessionId) {
|
|
|
1759
1828
|
}
|
|
1760
1829
|
|
|
1761
1830
|
async function openSession(id) {
|
|
1762
|
-
if (state.current && state.current !== id)
|
|
1831
|
+
if (state.current && state.current !== id) void archiveEmptySessionOnLeave(state.current,true)
|
|
1763
1832
|
state.current = id
|
|
1764
1833
|
setSessionRecovery('loading')
|
|
1765
1834
|
state.history = emptyDesktopHistory()
|
|
1835
|
+
const openingHistory=state.history, openingConnection=captureConnection()
|
|
1766
1836
|
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
1767
1837
|
showView('view-chat')
|
|
1768
1838
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
1769
1839
|
updateSessionActions()
|
|
1840
|
+
getConversationView().reset()
|
|
1770
1841
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
1771
1842
|
renderSessions()
|
|
1772
1843
|
renderSessionCards()
|
|
@@ -1774,6 +1845,8 @@ async function openSession(id) {
|
|
|
1774
1845
|
renderSessionPendingDesktop()
|
|
1775
1846
|
updateComposerStatus()
|
|
1776
1847
|
void refreshCompactionStatus(id)
|
|
1848
|
+
await restoreCachedHistory()
|
|
1849
|
+
if(state.current!==id||state.history!==openingHistory||!openingConnection.valid())return
|
|
1777
1850
|
await loadHistory()
|
|
1778
1851
|
}
|
|
1779
1852
|
async function closeSession() {
|
|
@@ -1791,52 +1864,112 @@ async function closeSession() {
|
|
|
1791
1864
|
updateSessionActions()
|
|
1792
1865
|
showView('view-sessions')
|
|
1793
1866
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1867
|
+
|
|
1868
|
+
function restoreCachedSessionList() {
|
|
1869
|
+
if(state.sessions.length)return
|
|
1870
|
+
try{const items=JSON.parse(LS.get(historyCacheScope()+':sessions',''));if(!Array.isArray(items))return;state.sessions=items.filter(item=>typeof item?.sessionId==='string').slice(0,80);state.byId=new Map(state.sessions.map(item=>[item.sessionId,item]));renderSessions()}catch{}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
let conversationView = null
|
|
1874
|
+
function historyCacheScope() { const hash=new SHA256();hash.update(new TextEncoder().encode(JSON.stringify([state.server||location.origin,state.token])));return 'historyCacheV2:'+hash.hex() }
|
|
1875
|
+
function historySource(value) {
|
|
1876
|
+
state.history.source=value
|
|
1877
|
+
const el=$('history-source')
|
|
1878
|
+
if(el)el.textContent=t(({cache:'ds.historyCached',syncing:'ds.historySyncing',synced:'ds.historySynced',saved:'ds.historySaved',offline:'ds.historyOffline',saveFailed:'ds.historyCacheFailed'})[value]||'ds.historySynced')
|
|
1879
|
+
}
|
|
1880
|
+
function getConversationView() {
|
|
1881
|
+
if(!conversationView)conversationView=new window.DshHistory.HistoryView($('history'),{
|
|
1882
|
+
html:entry=>eventHtml(entry),
|
|
1883
|
+
older:()=>loadHistory(false),olderLabel:()=>t('ds.historyEarlier'),loadingLabel:()=>t('ds.historyLoadingEarlier'),live:partialReasoningHtml,
|
|
1884
|
+
changed:view=>{state.history.renderStart=view.start;state.history.renderEnd=view.end;$('btn-history-latest')?.classList.toggle('hidden',!view.reading&&view.end>=view.entries.length&&!state.history.tailEvicted);},
|
|
1885
|
+
scrolled:()=>{}
|
|
1886
|
+
})
|
|
1887
|
+
return conversationView
|
|
1888
|
+
}
|
|
1889
|
+
function scheduleHistoryCacheSave() {
|
|
1890
|
+
clearTimeout(scheduleHistoryCacheSave._t)
|
|
1891
|
+
const connection=captureConnection(),id=state.current,history=state.history,scope=historyCacheScope()
|
|
1892
|
+
scheduleHistoryCacheSave._t=setTimeout(()=>{if(connection.valid()&&state.current===id&&state.history===history)void saveHistoryCache(scope,id,history)},500)
|
|
1893
|
+
}
|
|
1894
|
+
async function saveHistoryCache(scope=historyCacheScope(),id=state.current,history=state.history) {
|
|
1895
|
+
if(!id||!history.visible.length||history.tailEvicted)return
|
|
1896
|
+
const saved=await window.DshHistory.cacheSave(scope,id,{title:titleOf(state.byId.get(id)),events:history.visible.slice(-250),minSeq:history.visible.slice(-250)[0]?.seq??history.minSeq,hasMore:history.hasMore||history.visible.length>250,partialReasoning:[...history.partialReasoning.values()]})
|
|
1897
|
+
if(!saved&&state.current===id&&state.history===history&&scope===historyCacheScope())historySource('saveFailed')
|
|
1898
|
+
if(saved&&state.current===id&&state.history===history&&scope===historyCacheScope()&&['synced','saveFailed'].includes(history.source))historySource('saved')
|
|
1899
|
+
}
|
|
1900
|
+
async function restoreCachedHistory() {
|
|
1901
|
+
const connection=captureConnection(),id=state.current,history=state.history,scope=historyCacheScope()
|
|
1902
|
+
if(!id)return false
|
|
1903
|
+
const cached=await window.DshHistory.cacheLoad(scope,id,legacyId=>window.DshHistory.decodeLegacy(LS.get(scope,''),legacyId))
|
|
1904
|
+
if(!connection.valid()||state.current!==id||state.history!==history||!cached?.events?.length)return false
|
|
1905
|
+
history.visible=cached.events.slice(-250).filter(entry=>entry?.seq!=null&&shouldShowEvent(entry.event?.type,entry.event)).sort((a,b)=>a.seq-b.seq)
|
|
1906
|
+
history.seqs=new Set(history.visible.map(entry=>entry.seq));history.loaded=true;history.cached=true;history.hasMore=cached.events.length>250||cached.hasMore!==false;history.minSeq=cached.events.length>250?(history.visible[0]?.seq??Infinity):Number.isFinite(cached.minSeq)?cached.minSeq:history.visible[0]?.seq??Infinity
|
|
1907
|
+
if(cached.title&&!hasSessionTitle(state.byId.get(id)))hydrateSessionProjections(id,{values:{title:cached.title},asOfSeq:0})
|
|
1908
|
+
applyReasoningBaseline(cached.partialReasoning||[]);historySource('cache');setSessionRecovery('cached');renderHistory(true)
|
|
1909
|
+
return true
|
|
1910
|
+
}
|
|
1911
|
+
function trimVisible(direction='tail') {
|
|
1912
|
+
const h=state.history,excess=h.visible.length-5000
|
|
1913
|
+
if(excess<=0)return
|
|
1914
|
+
const older=direction==='older'||conversationView?.reading
|
|
1915
|
+
const removed=older?h.visible.splice(5000):h.visible.splice(0,excess)
|
|
1916
|
+
for(const entry of removed)h.seqs.delete(entry.seq)
|
|
1917
|
+
if(older)h.tailEvicted=true
|
|
1918
|
+
else{h.minSeq=h.visible[0]?.seq??Infinity;h.headEvicted=true;h.hasMore=true}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
async function loadHistory(reset = true) {
|
|
1922
|
+
const connection=captureConnection(),id=state.current,history=state.history
|
|
1923
|
+
if(!id||history.loading||(!reset&&Date.now()<(history.retryAfter||0)))return
|
|
1924
|
+
const reasoningVersion=history.reasoningVersion||0,hadContent=history.visible.length>0
|
|
1925
|
+
history.loading=true
|
|
1926
|
+
if(reset){if(hadContent)historySource('syncing');else setSessionRecovery('loading')}
|
|
1927
|
+
const payload={sessionId:id,maxMessages:60}
|
|
1928
|
+
if(!reset&&Number.isFinite(history.minSeq))payload.beforeSeq=history.minSeq
|
|
1929
|
+
getConversationView().more.disabled=true
|
|
1930
|
+
getConversationView().more.textContent=getConversationView().options.loadingLabel()
|
|
1801
1931
|
let v
|
|
1802
|
-
try
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
if
|
|
1807
|
-
setSessionRecovery('error',
|
|
1808
|
-
$('history').innerHTML
|
|
1932
|
+
try{v=await rpc('session.history',payload)}catch(error){
|
|
1933
|
+
if(!connection.valid()||state.current!==id||state.history!==history)return
|
|
1934
|
+
history.loading=false;history.retryAfter=Date.now()+1500
|
|
1935
|
+
if(error.message==='AUTH'){return}
|
|
1936
|
+
if(hadContent){historySource('offline');setSessionRecovery('cached',error.message);renderHistory(false,'fixed');return}
|
|
1937
|
+
setSessionRecovery('error',error.message)
|
|
1938
|
+
$('history').innerHTML='<div class="history-empty">'+esc(error.message)+'</div>'
|
|
1939
|
+
const retry=document.createElement('button');retry.type='button';retry.className='history-page-control';retry.textContent=t('ds.historyRetry');retry.addEventListener('click',()=>{void loadHistory(true)});$('history').append(retry)
|
|
1809
1940
|
return
|
|
1810
1941
|
}
|
|
1811
|
-
if
|
|
1812
|
-
const liveReasoning
|
|
1813
|
-
hydrateSessionProjections(id,
|
|
1942
|
+
if(!connection.valid()||state.current!==id||state.history!==history)return
|
|
1943
|
+
const liveReasoning=(history.reasoningVersion||0)!==reasoningVersion?new Map(history.partialReasoning):null
|
|
1944
|
+
hydrateSessionProjections(id,v.projections);history.loaded=true
|
|
1945
|
+
const incoming=v.events||[],prepared=[]
|
|
1814
1946
|
for (const entry of v.events || []) {
|
|
1815
|
-
const ev
|
|
1816
|
-
const seq = ev?.seq
|
|
1817
|
-
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
1947
|
+
const ev=entry?.event
|
|
1818
1948
|
applyReasoningStreamEvent(ev)
|
|
1819
|
-
if
|
|
1820
|
-
if
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
setSessionRecovery('ready')
|
|
1830
|
-
updateSessionActions()
|
|
1831
|
-
renderHistory()
|
|
1949
|
+
if(ev?.type==='turn/start'||ev?.type==='turn/end')noteSessionTurnTime(id,ev)
|
|
1950
|
+
if(ev?.seq==null||!shouldShowEvent(ev.type,ev))continue
|
|
1951
|
+
prepared.push({seq:ev.seq,event:ev,view:entry.view})
|
|
1952
|
+
}
|
|
1953
|
+
const rawSeqs=incoming.map(entry=>entry?.event?.seq).filter(Number.isFinite),floor=rawSeqs.length?Math.min(...rawSeqs):Infinity
|
|
1954
|
+
if(reset){const retainedOlder=!!v.hasMore&&history.visible.some(entry=>entry.seq<floor);const ceiling=rawSeqs.length?Math.max(...rawSeqs)+1:Infinity;history.visible=rawSeqs.length?window.DshHistory.reconcile(history.visible,prepared,floor,!!v.hasMore,ceiling):[];history.minSeq=retainedOlder?Math.min(history.minSeq,floor):floor;history.tailEvicted=false}
|
|
1955
|
+
else{history.visible=window.DshHistory.reconcile(history.visible,prepared,floor,true,payload.beforeSeq);history.minSeq=Math.min(history.minSeq,floor)}
|
|
1956
|
+
history.seqs=new Set(history.visible.map(entry=>entry.seq));history.headEvicted=false;trimVisible(reset?'tail':'older')
|
|
1957
|
+
if(liveReasoning)history.partialReasoning=liveReasoning;else applyReasoningBaseline(v.partialReasoning)
|
|
1958
|
+
history.hasMore=(!!v.hasMore||(reset&&history.headEvicted)) && rawSeqs.length>0 && (reset || floor<payload.beforeSeq)
|
|
1959
|
+
history.loading=false;history.cached=false;historySource('synced');setSessionRecovery('ready')
|
|
1960
|
+
updateSessionActions();renderSessionCards()
|
|
1961
|
+
renderHistory(!hadContent,reset?'fixed':'keep')
|
|
1962
|
+
|
|
1963
|
+
scheduleHistoryCacheSave()
|
|
1832
1964
|
}
|
|
1833
1965
|
|
|
1966
|
+
|
|
1834
1967
|
const INTERESTING_EVENTS = new Set([
|
|
1835
1968
|
'user/message', 'assistant/message', 'tool/call', 'tool/result',
|
|
1836
1969
|
'agent/status', 'checkpoint/created', 'compaction/complete', 'compaction/summary',
|
|
1837
1970
|
'goal/created', 'goal/updated', 'goal/completed', 'goal/cleared',
|
|
1838
1971
|
'todo/updated', 'plan/updated', 'question/asked', 'question/resolved',
|
|
1839
|
-
'approval/asked', 'approval/resolved'
|
|
1972
|
+
'approval/asked', 'approval/resolved'
|
|
1840
1973
|
])
|
|
1841
1974
|
function messageSource(data) {
|
|
1842
1975
|
const source = data?.source ?? data?.message?.source
|
|
@@ -1961,17 +2094,15 @@ function eventHtml(entry) {
|
|
|
1961
2094
|
if (type === 'question/asked') return `<div class="ds-tool">❓ ${esc(data.question || '')}</div>`
|
|
1962
2095
|
return `<div class="ds-tool">${esc(type)}</div>`
|
|
1963
2096
|
}
|
|
1964
|
-
function renderHistory() {
|
|
1965
|
-
|
|
1966
|
-
const items = state.history.visible
|
|
1967
|
-
const html = items.map(eventHtml).join('') + partialReasoningHtml()
|
|
1968
|
-
box.innerHTML = html || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
|
|
1969
|
-
box.scrollTop = box.scrollHeight
|
|
2097
|
+
function renderHistory(reset=false, mode='auto') {
|
|
2098
|
+
getConversationView().set(state.history.visible,{reset,follow:mode==='bottom',live:partialReasoningHtml(),hasMore:state.history.hasMore,loading:state.history.loading,emptyLabel:t('ds.historyEmpty')})
|
|
1970
2099
|
}
|
|
1971
2100
|
|
|
1972
2101
|
/* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
|
|
1973
2102
|
let sessionCardsRenderGeneration = 0
|
|
1974
2103
|
async function renderSessionCards() {
|
|
2104
|
+
window.DshInsights?.update()
|
|
2105
|
+
$('btn-insights')?.classList.toggle('hidden', !state.current)
|
|
1975
2106
|
const renderGeneration = ++sessionCardsRenderGeneration
|
|
1976
2107
|
const sessionId = state.current
|
|
1977
2108
|
const box = $('session-cards')
|
|
@@ -2514,8 +2645,10 @@ function resetFsForServerDesktop() {
|
|
|
2514
2645
|
if (select) select.classList.add('hidden')
|
|
2515
2646
|
}
|
|
2516
2647
|
async function openWorkspaceModal() {
|
|
2648
|
+
const connection = captureConnection()
|
|
2517
2649
|
if (!state.token) { toast(t('ds.toastAuth'), 'err'); showView('view-settings'); return }
|
|
2518
2650
|
if (!state.fs.path) await loadFs(null, true)
|
|
2651
|
+
if (!connection.valid()) return
|
|
2519
2652
|
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
2520
2653
|
$('workspace-name').value = ''
|
|
2521
2654
|
$('modal-workspace').classList.remove('hidden')
|
|
@@ -2526,31 +2659,39 @@ async function createWorkspace() {
|
|
|
2526
2659
|
if (createWorkspace.busy) return
|
|
2527
2660
|
const name = $('workspace-name').value.trim()
|
|
2528
2661
|
if (!name) { toast(t('ds.workspaceNameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
2662
|
+
const connection = captureConnection()
|
|
2529
2663
|
createWorkspace.busy = true
|
|
2530
2664
|
const parent = state.fs.path || ''
|
|
2531
2665
|
const button = $('workspace-create')
|
|
2532
2666
|
button.disabled = true
|
|
2533
2667
|
try {
|
|
2534
2668
|
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
2669
|
+
if (!connection.valid()) return
|
|
2535
2670
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2536
2671
|
const data = await res.json().catch(() => ({}))
|
|
2672
|
+
if (!connection.valid()) return
|
|
2537
2673
|
if (!res.ok) {
|
|
2538
2674
|
const msg = data.error === 'exists' ? t('ds.workspaceExists') : data.error === 'bad-name' ? t('ds.workspaceInvalidName') : data.error || ('HTTP ' + res.status)
|
|
2539
2675
|
throw new Error(msg)
|
|
2540
2676
|
}
|
|
2541
2677
|
closeWorkspaceModal()
|
|
2542
2678
|
await loadFs(parent || null, true)
|
|
2679
|
+
if (!connection.valid()) return
|
|
2543
2680
|
let workspace = null
|
|
2544
2681
|
try {
|
|
2545
2682
|
const created = await rpc('workspace.create', { path: data.path })
|
|
2683
|
+
if (!connection.valid()) return
|
|
2546
2684
|
workspace = created?.workspace || null
|
|
2547
2685
|
} catch {}
|
|
2686
|
+
if (!connection.valid()) return
|
|
2548
2687
|
if (workspace?.workspaceId) {
|
|
2549
2688
|
state.workspaces = [...state.workspaces.filter(item => item.workspaceId !== workspace.workspaceId), workspace]
|
|
2550
2689
|
}
|
|
2551
2690
|
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
2552
2691
|
const v = await safeRpc('session.create', sessionPayload, t('ds.toastOpFailed'))
|
|
2692
|
+
if (!connection.valid()) return
|
|
2553
2693
|
await refreshSessions()
|
|
2694
|
+
if (!connection.valid()) return
|
|
2554
2695
|
if (v?.sessionId) {
|
|
2555
2696
|
if (workspace?.workspaceId) LS.set('lastNewSessionWorkspaceV1', workspace.workspaceId)
|
|
2556
2697
|
toast(t('ds.workspaceCreated'), 'ok')
|
|
@@ -2559,6 +2700,7 @@ async function createWorkspace() {
|
|
|
2559
2700
|
toast(t('ds.workspaceCreatedNoSession'), 'ok')
|
|
2560
2701
|
}
|
|
2561
2702
|
} catch (e) {
|
|
2703
|
+
if (!connection.valid()) return
|
|
2562
2704
|
toast(`${t('ds.workspaceCreateFailed')}:${e.message || t('ds.feedbackNetworkError')}`, 'err')
|
|
2563
2705
|
} finally {
|
|
2564
2706
|
createWorkspace.busy = false
|
|
@@ -2566,6 +2708,8 @@ async function createWorkspace() {
|
|
|
2566
2708
|
}
|
|
2567
2709
|
}
|
|
2568
2710
|
async function loadFs(dir, silent) {
|
|
2711
|
+
const connection = captureConnection(), request = ++filesRequest
|
|
2712
|
+
const valid = () => connection.valid() && request === filesRequest
|
|
2569
2713
|
if (!state.token) {
|
|
2570
2714
|
$('fs-path').textContent = t('ds.toastAuth')
|
|
2571
2715
|
$('fs-list').innerHTML = `<div class="ds-empty">${t('ds.toastAuth')}</div>`
|
|
@@ -2578,8 +2722,10 @@ async function loadFs(dir, silent) {
|
|
|
2578
2722
|
}
|
|
2579
2723
|
try {
|
|
2580
2724
|
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
2725
|
+
if (!valid()) return
|
|
2581
2726
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
2582
2727
|
const data = await res.json().catch(() => ({}))
|
|
2728
|
+
if (!valid()) return
|
|
2583
2729
|
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
2584
2730
|
state.fs.path = data.path
|
|
2585
2731
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
@@ -2601,6 +2747,7 @@ async function loadFs(dir, silent) {
|
|
|
2601
2747
|
else window.open(fsApiUrl('/file', { path: row.dataset.fsPath, token: state.token }), '_blank')
|
|
2602
2748
|
}))
|
|
2603
2749
|
} catch (e) {
|
|
2750
|
+
if (!valid()) return
|
|
2604
2751
|
$('fs-path').textContent = target || '~'
|
|
2605
2752
|
$('fs-list').innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
2606
2753
|
}
|
|
@@ -2742,17 +2889,22 @@ function commitWorkspaceSessionOrder(workspaceId, order) {
|
|
|
2742
2889
|
toast(t('ds.wbOrderSaved'), 'ok')
|
|
2743
2890
|
}
|
|
2744
2891
|
async function refreshWorkbench({ silent = false } = {}) {
|
|
2892
|
+
const connection = captureConnection(), request = ++workbenchRequest
|
|
2893
|
+
const valid = () => connection.valid() && request === workbenchRequest
|
|
2745
2894
|
if (!state.token) { renderWorkbench(); return }
|
|
2746
2895
|
let wb = null
|
|
2747
2896
|
try {
|
|
2748
2897
|
wb = await wbGateway('GET', '/workbench')
|
|
2898
|
+
if (!valid()) return
|
|
2749
2899
|
state.wb.apiMissing = false
|
|
2750
2900
|
} catch (e) {
|
|
2901
|
+
if (!valid()) return
|
|
2751
2902
|
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
2752
2903
|
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
2753
2904
|
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
2754
2905
|
}
|
|
2755
2906
|
const wl = await safeRpc('workspace.list', {}, '')
|
|
2907
|
+
if (!valid()) return
|
|
2756
2908
|
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
2757
2909
|
state.workspaces = wl && Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2758
2910
|
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
@@ -2769,8 +2921,10 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2769
2921
|
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
2770
2922
|
try {
|
|
2771
2923
|
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
2924
|
+
if (!valid()) return
|
|
2772
2925
|
if (listRes.ok) {
|
|
2773
2926
|
const listData = await listRes.json().catch(() => ({}))
|
|
2927
|
+
if (!valid()) return
|
|
2774
2928
|
if (Array.isArray(listData.entries)) {
|
|
2775
2929
|
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
2776
2930
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
@@ -2783,16 +2937,19 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
2783
2937
|
if (have.has(wbPathKey(projectPath))) continue
|
|
2784
2938
|
try {
|
|
2785
2939
|
const created = await rpc('workspace.create', { path: projectPath })
|
|
2940
|
+
if (!valid()) return
|
|
2786
2941
|
if (created?.workspace) {
|
|
2787
2942
|
items.push(created.workspace)
|
|
2788
2943
|
state.workspaces.push(created.workspace)
|
|
2789
2944
|
have.add(wbPathKey(projectPath))
|
|
2790
2945
|
}
|
|
2791
2946
|
} catch {}
|
|
2947
|
+
if (!valid()) return
|
|
2792
2948
|
}
|
|
2793
2949
|
}
|
|
2794
2950
|
}
|
|
2795
2951
|
} catch {}
|
|
2952
|
+
if (!valid()) return
|
|
2796
2953
|
state.wb.projects = orderedWorkspaceItems(items
|
|
2797
2954
|
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
2798
2955
|
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true })))
|
|
@@ -2824,7 +2981,7 @@ function renderWorkbench() {
|
|
|
2824
2981
|
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
2825
2982
|
html += projects.length ? projects.map(w => {
|
|
2826
2983
|
const id = String(w.workspaceId || '')
|
|
2827
|
-
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(
|
|
2984
|
+
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isVisibleSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => sessionSortTime(b) - sessionSortTime(a)))
|
|
2828
2985
|
const open = state.wb.open === id
|
|
2829
2986
|
return `<div class="ds-wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}" data-motion-key="${esc(id)}">
|
|
2830
2987
|
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
@@ -3069,35 +3226,88 @@ function renderStats(days) {
|
|
|
3069
3226
|
}
|
|
3070
3227
|
|
|
3071
3228
|
/* ---------------- 视图与连接状态 ---------------- */
|
|
3229
|
+
let overviewDetector = null
|
|
3230
|
+
let overviewPulse = null
|
|
3231
|
+
function setOverviewText(id,value){const node=$(id);if(overviewPulse)overviewPulse.text(node,value);else if(node)node.textContent=value}
|
|
3232
|
+
let overviewStarting = true
|
|
3233
|
+
function overviewChecks() {
|
|
3234
|
+
const confirmed=overviewDetector?.model.gateway===true,health=activeGatewayHealth()
|
|
3235
|
+
return {
|
|
3236
|
+
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)) && confirmed,
|
|
3237
|
+
dsh: dshReachable() && confirmed,
|
|
3238
|
+
mux: !!state.streamsOk?.mux && confirmed && health?.events?.mux?.connected!==false,
|
|
3239
|
+
host: !!state.streamsOk?.host && confirmed && health?.events?.host?.connected!==false
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function repairMissingStreams() {
|
|
3243
|
+
if(!state.token||!navigator.onLine||state.selectingServer||overviewStarting)return
|
|
3244
|
+
if(state.streamMode==='poll'){tryRestoreWs();return}
|
|
3245
|
+
for(const [kind,handler,refresh] of [['mux',onMuxFrame,true],['host',onHostFrame,false]]) {
|
|
3246
|
+
const ws=streams[kind]
|
|
3247
|
+
if((!ws||ws.readyState===WebSocket.CLOSED)&&!streamMeta[kind].retryTimer)openStream(kind,handler,refresh)
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async function probeOverviewLinks(signal) {
|
|
3251
|
+
const connection=captureConnection(),base=String(connection.server||location.origin).replace(/\/+$/,'')
|
|
3252
|
+
let timer
|
|
3253
|
+
const controller=new AbortController(),cancel=()=>controller.abort()
|
|
3254
|
+
signal.addEventListener('abort',cancel,{once:true});if(signal.aborted)controller.abort()
|
|
3255
|
+
timer=setTimeout(cancel,4000)
|
|
3256
|
+
const healthRequest=fetch(base+'/health?t='+Date.now(),{signal:controller.signal,cache:'no-store'}).then(async response=>{
|
|
3257
|
+
if(!response.ok)throw Error('health')
|
|
3258
|
+
const health=await response.json();if(health?.ok!==true)throw Error('health')
|
|
3259
|
+
return health
|
|
3260
|
+
}).catch(()=>null).finally(()=>{clearTimeout(timer);signal.removeEventListener('abort',cancel)})
|
|
3261
|
+
const [health,host]=await Promise.all([healthRequest,refreshHostDescriptionDesktop()])
|
|
3262
|
+
if(!connection.valid()||signal.aborted)throw Error('stale check')
|
|
3263
|
+
if(health){if(host)health.upstreamReachable=true;state.gatewayHealth[base]=health}
|
|
3264
|
+
else delete state.gatewayHealth[base]
|
|
3265
|
+
return {gateway:!!health}
|
|
3266
|
+
}
|
|
3267
|
+
function startOverviewDetection(force=false,identityChanged=false) {
|
|
3268
|
+
if(identityChanged)overviewDetector?.pause()
|
|
3269
|
+
if(!window.DshLinkCheck)return
|
|
3270
|
+
if(!overviewDetector)overviewDetector=new window.DshLinkCheck.LinkCheck({
|
|
3271
|
+
active:()=>document.visibilityState==='visible'&&!$('view-overview').classList.contains('hidden'),
|
|
3272
|
+
configured:()=>!!state.token&&(!!state.server||/^https?:$/.test(location.protocol)),online:()=>navigator.onLine,
|
|
3273
|
+
snapshot:overviewChecks,repair:repairMissingStreams,probe:probeOverviewLinks,changed:renderOverviewDesktop
|
|
3274
|
+
})
|
|
3275
|
+
overviewDetector.start(force)
|
|
3276
|
+
}
|
|
3277
|
+
window.addEventListener('online',()=>startOverviewDetection(true))
|
|
3278
|
+
window.addEventListener('offline',()=>startOverviewDetection(true))
|
|
3279
|
+
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')startOverviewDetection(true);else {overviewDetector?.pause();overviewPulse?.pause()}})
|
|
3280
|
+
|
|
3072
3281
|
function renderOverviewDesktop() {
|
|
3073
3282
|
const ring = $('ds-overview-pulse-ring')
|
|
3074
3283
|
if (!ring) return
|
|
3075
|
-
const checks =
|
|
3076
|
-
|
|
3077
|
-
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
|
|
3078
|
-
dsh: dshReachable(),
|
|
3079
|
-
mux: !!state.streamsOk?.mux,
|
|
3080
|
-
host: !!state.streamsOk?.host
|
|
3081
|
-
}
|
|
3284
|
+
const checks = overviewChecks()
|
|
3285
|
+
const detection=overviewDetector?.model,checking=detection?.phase==='checking'||(detection?.phase==='degraded'&&detection?.probing===true)
|
|
3082
3286
|
const online = Object.values(checks).filter(Boolean).length
|
|
3287
|
+
$('ds-overview-refresh').disabled=detection?.probing===true
|
|
3083
3288
|
const status = online === 4 ? 'Nominal' : online > 0 ? 'Degraded' : 'Offline'
|
|
3084
3289
|
const pulseCard = document.querySelector('.ds-overview-pulse-card')
|
|
3085
3290
|
if (pulseCard) {
|
|
3291
|
+
if(!overviewPulse&&window.DshLinkCheck?.PulseVisual)overviewPulse=new window.DshLinkCheck.PulseVisual({card:pulseCard,active:()=>document.visibilityState==='visible'&&!$('view-overview').classList.contains('hidden')})
|
|
3292
|
+
overviewPulse?.setChecking(checking)
|
|
3293
|
+
overviewPulse?.setLinks(checks)
|
|
3086
3294
|
pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
|
|
3295
|
+
pulseCard.classList.toggle('status-checking',checking)
|
|
3296
|
+
pulseCard.setAttribute('aria-busy',String(checking))
|
|
3087
3297
|
pulseCard.classList.add('status-' + status.toLowerCase())
|
|
3088
3298
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
$('ds-overview-status-desc').textContent = t('ds.components', { n: online })
|
|
3299
|
+
setOverviewText('ds-overview-health',checking ? t('ds.checking') : online === 4 ? t('ds.live') : online ? `${online}/4` : t('ds.offlineCore'))
|
|
3300
|
+
setOverviewText('ds-overview-health-caption',checking ? `${online}/4` : online === 4 ? t('ds.allLinked') : online ? t('ds.confirmedShort') : t('ds.offlineShort'))
|
|
3301
|
+
setOverviewText('ds-overview-status',checking ? t('ds.checking') : t(`ds.system${status}`))
|
|
3302
|
+
setOverviewText('ds-overview-status-desc',checking ? t('ds.autoChecking',{n:online}) : detection?.phase==='degraded' ? t('ds.autoRetry',{n:online}) : t('ds.components',{n:online}))
|
|
3094
3303
|
for (const [name, ok] of Object.entries(checks)) {
|
|
3095
3304
|
const item = document.querySelector(`[data-ds-overview-link="${name}"]`)
|
|
3096
3305
|
if (!item) continue
|
|
3097
3306
|
item.classList.toggle('ok', ok)
|
|
3098
|
-
item.classList.toggle('off', !ok)
|
|
3307
|
+
item.classList.toggle('off', !ok&&!checking)
|
|
3308
|
+
item.classList.toggle('pending',!ok&&checking)
|
|
3099
3309
|
const value = item.querySelector('b')
|
|
3100
|
-
if (value) value.
|
|
3310
|
+
if (value) setOverviewText(value.id || (value.id=`ds-overview-link-${name}`), ok ? t('ds.online') : checking ? t('ds.checkingShort') : t('ds.offlineShort'))
|
|
3101
3311
|
}
|
|
3102
3312
|
|
|
3103
3313
|
const pending = [
|
|
@@ -3169,19 +3379,28 @@ function setMobileSidebar(open) {
|
|
|
3169
3379
|
$('ds-app')?.classList.toggle('mobile-nav-open', visible)
|
|
3170
3380
|
}
|
|
3171
3381
|
|
|
3382
|
+
function openDesktopPlugins() {
|
|
3383
|
+
const connection=captureConnection()
|
|
3384
|
+
if(!connection.token){$('desktop-plugin-page').textContent=t('ds.toastAuth');return}
|
|
3385
|
+
window.DshPluginCenter.mount({url:path=>connection.server+path,headers:{authorization:'Bearer '+connection.token,'x-dsh-remote-client':'web',...clientIdHeaders()},valid:connection.valid},$('desktop-plugin-page'))
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3172
3388
|
function showView(id) {
|
|
3173
3389
|
if (state.view === 'view-chat' && id !== 'view-chat' && state.current) void archiveEmptySessionOnLeave(state.current)
|
|
3174
3390
|
state.view = id
|
|
3175
|
-
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
3391
|
+
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-plugins', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
3176
3392
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
3177
3393
|
if (narrowSidebarQuery.matches) setMobileSidebar(false)
|
|
3178
3394
|
window.DshMotion?.view($(id))
|
|
3179
|
-
const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
|
|
3395
|
+
const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings', 'view-plugins': 'ds.plugins' }
|
|
3180
3396
|
if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
|
|
3181
3397
|
else $('ds-title').textContent = t(titles[id])
|
|
3182
|
-
if (id === 'view-overview') renderOverviewDesktop()
|
|
3398
|
+
if (id === 'view-overview') {startOverviewDetection(true);renderOverviewDesktop()}
|
|
3399
|
+
else {overviewDetector?.pause();overviewPulse?.pause()}
|
|
3183
3400
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
|
|
3184
3401
|
if (id === 'view-settings') showSettingsHome()
|
|
3402
|
+
if (id === 'view-plugins') openDesktopPlugins()
|
|
3403
|
+
else window.DshPluginCenter?.close()
|
|
3185
3404
|
updateSessionActions()
|
|
3186
3405
|
}
|
|
3187
3406
|
|
|
@@ -3190,9 +3409,14 @@ function updateSessionActions() {
|
|
|
3190
3409
|
$('btn-rename-session')?.classList.toggle('hidden', !active)
|
|
3191
3410
|
$('btn-archive-session')?.classList.toggle('hidden', !active || state.archivedIds.includes(state.current))
|
|
3192
3411
|
$('ds-session-status')?.classList.toggle('hidden', !active || !recoveryLabel())
|
|
3412
|
+
if (!active) $('ds-workspace')?.classList.add('hidden')
|
|
3193
3413
|
if (active) {
|
|
3194
3414
|
const s = state.byId.get(state.current)
|
|
3195
3415
|
$('ds-title').textContent = s ? titleOf(s) : t('ds.sessions')
|
|
3416
|
+
const workspace = s?.cwd || ''
|
|
3417
|
+
$('ds-workspace').textContent = '📁 ' + (workspace.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || t('ds.noWorkspace')) + ' ›'
|
|
3418
|
+
$('ds-workspace').title = workspace
|
|
3419
|
+
$('ds-workspace').classList.remove('hidden')
|
|
3196
3420
|
$('ds-session-status').textContent = recoveryLabel()
|
|
3197
3421
|
}
|
|
3198
3422
|
const running = !!state.byId.get(state.current)?.running || (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
@@ -3214,6 +3438,7 @@ function showSettingsPage(name) {
|
|
|
3214
3438
|
if (name === 'general') renderPresetSummary()
|
|
3215
3439
|
}
|
|
3216
3440
|
function updateConn() {
|
|
3441
|
+
overviewDetector?.observe()
|
|
3217
3442
|
const el = $('conn-badge')
|
|
3218
3443
|
// 双实时通道可能按任意顺序打开,连接刷新必须同时刷新系统总览。
|
|
3219
3444
|
renderOverviewDesktop()
|
|
@@ -3259,7 +3484,23 @@ function updateConn() {
|
|
|
3259
3484
|
}
|
|
3260
3485
|
|
|
3261
3486
|
/* ---------------- 初始化 ---------------- */
|
|
3487
|
+
|
|
3488
|
+
function bindSessionDialogs() {
|
|
3489
|
+
const actions=$('session-actions'), info=$('session-info');
|
|
3490
|
+
$('btn-session-more').addEventListener('click',()=>actions.showModal());
|
|
3491
|
+
actions.addEventListener('click',e=>{if(e.target===actions)actions.close();else if(e.target.closest('button'))actions.close()},true);
|
|
3492
|
+
const openInfo=()=>{const s=state.byId.get(state.current);if(!s)return; $('session-info-name').textContent=titleOf(s);$('session-info-path').textContent=s.cwd||t('ds.noWorkspace');$('session-info-id').textContent=s.sessionId;$('btn-copy-session-workspace').disabled=!s.cwd;info.showModal()};
|
|
3493
|
+
$('ds-title').addEventListener('click',openInfo);
|
|
3494
|
+
$('ds-workspace').addEventListener('click',openInfo);
|
|
3495
|
+
$('btn-session-info').addEventListener('click',openInfo);
|
|
3496
|
+
info.addEventListener('click',e=>{if(e.target===info||e.target.closest('[data-session-dialog-close]'))info.close()});
|
|
3497
|
+
$('btn-copy-session-workspace').addEventListener('click',async()=>{const value=state.byId.get(state.current)?.cwd;if(value&&await copyText(value))toast(t('ds.copied'),'ok')});
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3262
3500
|
function bindUi() {
|
|
3501
|
+
bindSessionDialogs()
|
|
3502
|
+
getConversationView()
|
|
3503
|
+
$('btn-history-latest').addEventListener('click',()=>{getConversationView().latest();if(state.history.tailEvicted)void loadHistory(true)})
|
|
3263
3504
|
$('btn-new-session').addEventListener('click', openNewSessionModal)
|
|
3264
3505
|
$('btn-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
3265
3506
|
$('session-sort')?.addEventListener('change', (e) => {
|
|
@@ -3277,14 +3518,7 @@ function bindUi() {
|
|
|
3277
3518
|
}
|
|
3278
3519
|
})
|
|
3279
3520
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
|
|
3280
|
-
$('ds-overview-refresh').addEventListener('click',
|
|
3281
|
-
toast(t('ds.loading'))
|
|
3282
|
-
if (state.token) {
|
|
3283
|
-
await refreshSessions()
|
|
3284
|
-
await refreshHostDescriptionDesktop({ notify: true })
|
|
3285
|
-
}
|
|
3286
|
-
renderOverviewDesktop()
|
|
3287
|
-
})
|
|
3521
|
+
$('ds-overview-refresh').addEventListener('click',()=>{startOverviewDetection(true);if(state.token)void refreshSessions()})
|
|
3288
3522
|
$('ds-overview-primary-action').addEventListener('click', () => {
|
|
3289
3523
|
const button = $('ds-overview-primary-action')
|
|
3290
3524
|
const action = button.dataset.dsOverviewAction
|
|
@@ -3369,6 +3603,7 @@ function bindUi() {
|
|
|
3369
3603
|
if (!e.target.closest('#model-menu') && !e.target.closest('#btn-model')) $('model-menu')?.classList.add('hidden')
|
|
3370
3604
|
})
|
|
3371
3605
|
$('btn-stats-top').addEventListener('click', toggleStatsDrawer)
|
|
3606
|
+
$('btn-insights').addEventListener('click', () => openInsights())
|
|
3372
3607
|
$('stats-drawer-close').addEventListener('click', toggleStatsDrawer)
|
|
3373
3608
|
// 赞赏支持
|
|
3374
3609
|
$('btn-donate').addEventListener('click', openDonateModal)
|
|
@@ -3511,23 +3746,13 @@ async function start() {
|
|
|
3511
3746
|
checkNotesOnStart()
|
|
3512
3747
|
if (state.token) {
|
|
3513
3748
|
await selectFastestServer({ silent: true, reconnect: false })
|
|
3749
|
+
overviewStarting=false
|
|
3514
3750
|
openStreams()
|
|
3515
|
-
await refreshSessions()
|
|
3516
|
-
await refreshHostDescriptionDesktop()
|
|
3751
|
+
await Promise.all([refreshSessions(),refreshHostDescriptionDesktop()])
|
|
3517
3752
|
refreshWorkbench({ silent: true })
|
|
3518
3753
|
}
|
|
3754
|
+
overviewStarting=false
|
|
3519
3755
|
renderOverviewDesktop()
|
|
3520
3756
|
}
|
|
3521
3757
|
|
|
3522
3758
|
start()
|
|
3523
|
-
|
|
3524
|
-
// Capture the connection so switching servers cannot redirect a plugin mutation.
|
|
3525
|
-
document.getElementById('btn-plugin-center')?.addEventListener('click', () => {
|
|
3526
|
-
const server = state.server || ''
|
|
3527
|
-
const token = state.token
|
|
3528
|
-
window.DshPluginCenter.open({
|
|
3529
|
-
url: path => server + path,
|
|
3530
|
-
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
3531
|
-
valid: () => (state.server || '') === server && state.token === token,
|
|
3532
|
-
})
|
|
3533
|
-
})
|