dsh-remote-plugin 0.6.14 → 0.6.15
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 +202 -23
- package/package.json +1 -1
- package/public/app.js +260 -34
- package/public/desktop/desktop.css +22 -7
- package/public/desktop/desktop.html +24 -4
- package/public/desktop/desktop.js +275 -26
- package/public/index.html +25 -8
- package/public/morphicons-init.js +41 -0
- package/public/motion.js +469 -0
- package/public/plugin.html +4 -1
- package/public/plugin.js +1 -0
- package/public/styles.css +20 -6
- package/public/update.json +8 -8
- package/public/vendor/gsap/NOTICE.md +10 -0
- package/public/vendor/gsap/gsap.min.js +11 -0
- package/public/vendor/morphicons/LICENSE +21 -0
- package/public/vendor/morphicons/README.md +10 -0
- package/public/vendor/morphicons/controller-CXZuwJ_M.js +152 -0
- package/public/vendor/morphicons/dom.js +206 -0
- package/public/vendor/morphicons/element.js +261 -0
- package/public/vendor/morphicons/normalize-CYnN3Npw.js +540 -0
- package/public/vendor/morphicons/spring-CFHloqPP.js +623 -0
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -64,6 +64,9 @@ const state = {
|
|
|
64
64
|
workspaceFilter: LS.get('workspaceFilterV1', ''),
|
|
65
65
|
byId: new Map(),
|
|
66
66
|
current: null, // 当前打开的 sessionId
|
|
67
|
+
sessionRecovery: { status: 'idle', error: '' },
|
|
68
|
+
pendingProjections: new Map(),
|
|
69
|
+
lastStreamResyncAt: 0,
|
|
67
70
|
hostInfo: null,
|
|
68
71
|
localVersion: '',
|
|
69
72
|
updateInfo: null,
|
|
@@ -1082,6 +1085,7 @@ function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
|
1082
1085
|
renderPending()
|
|
1083
1086
|
}
|
|
1084
1087
|
if (refreshOnOpen) refreshAll()
|
|
1088
|
+
if (allStreamsOpen()) resyncAfterStreamOpen()
|
|
1085
1089
|
}
|
|
1086
1090
|
ws.onmessage = (msg) => {
|
|
1087
1091
|
if (!streamIsCurrent(kind, ws, generation)) return
|
|
@@ -1168,9 +1172,9 @@ async function pollKind(kind) {
|
|
|
1168
1172
|
const since = state.pollSeq[kind] || 0
|
|
1169
1173
|
let res
|
|
1170
1174
|
try {
|
|
1171
|
-
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(
|
|
1175
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(30000) : undefined
|
|
1172
1176
|
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
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 })
|
|
1177
|
+
res = signal ? await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}&wait=25000`), { signal, headers }) : await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}&wait=25000`), { headers })
|
|
1174
1178
|
} catch { return }
|
|
1175
1179
|
if (res.status === 401) { authFailure(); return }
|
|
1176
1180
|
if (!res.ok) return
|
|
@@ -1188,6 +1192,7 @@ async function pollKind(kind) {
|
|
|
1188
1192
|
renderPending()
|
|
1189
1193
|
}
|
|
1190
1194
|
scheduleRefresh()
|
|
1195
|
+
if (state.current) void resyncCurrentSession()
|
|
1191
1196
|
}
|
|
1192
1197
|
for (const item of data.events) {
|
|
1193
1198
|
if (item.seq > (state.pollSeq[kind] || 0)) {
|
|
@@ -1346,24 +1351,73 @@ async function refreshSessions() {
|
|
|
1346
1351
|
}
|
|
1347
1352
|
state.sessions = v.items || []
|
|
1348
1353
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1354
|
+
applyPendingProjections()
|
|
1349
1355
|
cacheWrite(CACHE.sessions, state.sessions.slice(0, 80))
|
|
1350
1356
|
renderSessions()
|
|
1351
1357
|
refreshWorkbench()
|
|
1352
1358
|
}
|
|
1353
1359
|
|
|
1354
1360
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1361
|
+
function hydrateSessionProjections(sessionId, projections) {
|
|
1362
|
+
const s = state.byId.get(sessionId)
|
|
1363
|
+
if (!s || !projections || typeof projections !== 'object') return
|
|
1364
|
+
const incomingSeq = Number(projections.asOfSeq) || 0
|
|
1365
|
+
const current = s.projections || { asOfSeq: 0, values: {} }
|
|
1366
|
+
const currentSeq = Number(current.asOfSeq) || 0
|
|
1367
|
+
if (incomingSeq < currentSeq) return
|
|
1368
|
+
s.projections = {
|
|
1369
|
+
asOfSeq: Math.max(currentSeq, incomingSeq),
|
|
1370
|
+
values: { ...(current.values || {}), ...(projections.values || {}) }
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function applyPendingProjections() {
|
|
1374
|
+
for (const [sessionId, projections] of state.pendingProjections) {
|
|
1375
|
+
if (!state.byId.has(sessionId)) continue
|
|
1376
|
+
hydrateSessionProjections(sessionId, projections)
|
|
1377
|
+
state.pendingProjections.delete(sessionId)
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1355
1380
|
function applyProjection(sessionId, key, value, seq) {
|
|
1356
1381
|
const s = state.byId.get(sessionId)
|
|
1357
|
-
if (s) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1382
|
+
if (!s) {
|
|
1383
|
+
const pending = state.pendingProjections.get(sessionId) || { asOfSeq: 0, values: {} }
|
|
1384
|
+
pending.values[key] = value
|
|
1385
|
+
pending.asOfSeq = Math.max(pending.asOfSeq || 0, seq || 0)
|
|
1386
|
+
state.pendingProjections.set(sessionId, pending)
|
|
1387
|
+
return
|
|
1362
1388
|
}
|
|
1389
|
+
const currentSeq = Number(s.projections?.asOfSeq) || 0
|
|
1390
|
+
if (seq && seq < currentSeq) return
|
|
1391
|
+
s.projections = s.projections || { asOfSeq: 0, values: {} }
|
|
1392
|
+
s.projections.values = s.projections.values || {}
|
|
1393
|
+
s.projections.values[key] = value
|
|
1394
|
+
s.projections.asOfSeq = Math.max(currentSeq, seq || 0)
|
|
1363
1395
|
if (state.current === sessionId) { renderSessionTitle(); renderSessionCards() }
|
|
1364
1396
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) scheduleRefresh()
|
|
1365
1397
|
else renderSessions()
|
|
1366
1398
|
}
|
|
1399
|
+
function setSessionRecovery(status, error = '') {
|
|
1400
|
+
state.sessionRecovery = { status, error: String(error || '') }
|
|
1401
|
+
if (state.current) { renderSessionSub(); updateSessionStatus() }
|
|
1402
|
+
}
|
|
1403
|
+
function recoveryLabel() {
|
|
1404
|
+
const status = state.sessionRecovery.status
|
|
1405
|
+
if (status === 'loading' || status === 'resuming') return t('session.recovering')
|
|
1406
|
+
if (status === 'cached') return t('session.recoveryCached')
|
|
1407
|
+
if (status === 'error') return t('session.recoveryFailed')
|
|
1408
|
+
return ''
|
|
1409
|
+
}
|
|
1410
|
+
function resyncCurrentSession() {
|
|
1411
|
+
if (!state.current) return Promise.resolve()
|
|
1412
|
+
return loadHistory(true).then(() => {
|
|
1413
|
+
if (state.current) { renderSessionCards(); renderSessionSub(); updateCancelBtn(); updateSessionStatus() }
|
|
1414
|
+
})
|
|
1415
|
+
}
|
|
1416
|
+
function resyncAfterStreamOpen() {
|
|
1417
|
+
if (!state.current || Date.now() - state.lastStreamResyncAt < 1200) return
|
|
1418
|
+
state.lastStreamResyncAt = Date.now()
|
|
1419
|
+
void refreshAll().then(() => resyncCurrentSession())
|
|
1420
|
+
}
|
|
1367
1421
|
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('session.unknown')) }
|
|
1368
1422
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
1369
1423
|
function isTopLevelSession(session) {
|
|
@@ -1415,6 +1469,63 @@ const WORKSPACE_UNGROUPED = '__ungrouped__'
|
|
|
1415
1469
|
function workspaceItems() {
|
|
1416
1470
|
return (state.wbProjects || []).filter(w => w && typeof w.workspaceId === 'string' && w.workspaceId && typeof w.path === 'string' && w.path)
|
|
1417
1471
|
}
|
|
1472
|
+
const WORKBENCH_ORDER_CACHE_KEY = 'workbenchOrderV1'
|
|
1473
|
+
function workbenchOrderScope() { return String(state.server || location.origin || 'default') }
|
|
1474
|
+
function workbenchOrderStore() {
|
|
1475
|
+
const value = cacheRead(WORKBENCH_ORDER_CACHE_KEY, {})
|
|
1476
|
+
if (!value || typeof value !== 'object') return { scopes: {} }
|
|
1477
|
+
if (!value.scopes || typeof value.scopes !== 'object') value.scopes = {}
|
|
1478
|
+
return value
|
|
1479
|
+
}
|
|
1480
|
+
function workbenchOrderScopeValue() {
|
|
1481
|
+
const store = workbenchOrderStore()
|
|
1482
|
+
const key = workbenchOrderScope()
|
|
1483
|
+
if (!store.scopes[key] || typeof store.scopes[key] !== 'object') store.scopes[key] = {}
|
|
1484
|
+
return { store, value: store.scopes[key] }
|
|
1485
|
+
}
|
|
1486
|
+
function orderedItems(items, ids, getId) {
|
|
1487
|
+
const source = Array.isArray(items) ? items : []
|
|
1488
|
+
const byId = new Map(source.map(item => [String(getId(item)), item]))
|
|
1489
|
+
const result = []
|
|
1490
|
+
const used = new Set()
|
|
1491
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
1492
|
+
const key = String(id)
|
|
1493
|
+
const item = byId.get(key)
|
|
1494
|
+
if (item && !used.has(key)) { result.push(item); used.add(key) }
|
|
1495
|
+
}
|
|
1496
|
+
for (const item of source) {
|
|
1497
|
+
const key = String(getId(item))
|
|
1498
|
+
if (!used.has(key)) { result.push(item); used.add(key) }
|
|
1499
|
+
}
|
|
1500
|
+
return result
|
|
1501
|
+
}
|
|
1502
|
+
function orderedWorkspaceItems(items) {
|
|
1503
|
+
const { value } = workbenchOrderScopeValue()
|
|
1504
|
+
return orderedItems(items, value.workspaceIds, item => item.workspaceId)
|
|
1505
|
+
}
|
|
1506
|
+
function orderedWorkspaceSessions(workspaceId, items) {
|
|
1507
|
+
const { value } = workbenchOrderScopeValue()
|
|
1508
|
+
return orderedItems(items, value.sessionIds?.[String(workspaceId)], item => item.sessionId)
|
|
1509
|
+
}
|
|
1510
|
+
function saveWorkbenchOrder(mutator) {
|
|
1511
|
+
const { store, value } = workbenchOrderScopeValue()
|
|
1512
|
+
mutator(value)
|
|
1513
|
+
cacheWrite(WORKBENCH_ORDER_CACHE_KEY, store)
|
|
1514
|
+
}
|
|
1515
|
+
function commitWorkspaceOrder(order) {
|
|
1516
|
+
saveWorkbenchOrder(value => { value.workspaceIds = order.map(String) })
|
|
1517
|
+
renderWorkbench()
|
|
1518
|
+
toast(t('wb.orderSaved'), 'ok')
|
|
1519
|
+
}
|
|
1520
|
+
function commitWorkspaceSessionOrder(workspaceId, order) {
|
|
1521
|
+
if (!workspaceId) return
|
|
1522
|
+
saveWorkbenchOrder(value => {
|
|
1523
|
+
value.sessionIds ||= {}
|
|
1524
|
+
value.sessionIds[String(workspaceId)] = order.map(String)
|
|
1525
|
+
})
|
|
1526
|
+
renderWorkbench()
|
|
1527
|
+
toast(t('wb.orderSaved'), 'ok')
|
|
1528
|
+
}
|
|
1418
1529
|
function workspaceById(workspaceId) {
|
|
1419
1530
|
return workspaceItems().find(w => w.workspaceId === workspaceId) || null
|
|
1420
1531
|
}
|
|
@@ -1541,32 +1652,48 @@ function renderWorkbench() {
|
|
|
1541
1652
|
toggle.setAttribute('aria-expanded', state.wbOpen ? 'true' : 'false')
|
|
1542
1653
|
panel.classList.toggle('hidden', !state.wbOpen)
|
|
1543
1654
|
if (!state.wbOpen) { panel.innerHTML = ''; return }
|
|
1544
|
-
const projects = state.wbProjects.filter(w => wbStrictInside(w.path, workbenchRoot()))
|
|
1655
|
+
const projects = orderedWorkspaceItems(state.wbProjects.filter(w => wbStrictInside(w.path, workbenchRoot())))
|
|
1545
1656
|
if (!projects.length) {
|
|
1546
1657
|
panel.innerHTML = `<div class="wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1547
1658
|
return
|
|
1548
1659
|
}
|
|
1549
1660
|
const archivedSet = new Set(state.wbArchived || [])
|
|
1550
|
-
|
|
1661
|
+
const projectHtml = projects.map(w => {
|
|
1551
1662
|
const id = String(w.workspaceId || '')
|
|
1552
1663
|
const open = !!state.wbOpenProjects[id]
|
|
1553
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId))
|
|
1664
|
+
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)))
|
|
1554
1665
|
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
1555
1666
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1556
|
-
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
|
|
1667
|
+
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
|
|
1668
|
+
<span class="wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
|
|
1557
1669
|
<span class="wb-session-title">${esc(titleOf(s))}</span>
|
|
1558
1670
|
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(sessionSortTime(s)))}</span>
|
|
1559
1671
|
</button>
|
|
1560
1672
|
<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
|
|
1561
1673
|
</div>`).join('') : `<div class="wb-empty">${esc(t('wb.noSessions'))}</div>`}</div>` : ''
|
|
1562
|
-
return `<div class="wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}">
|
|
1674
|
+
return `<div class="wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}" data-motion-key="${esc(id)}">
|
|
1563
1675
|
<div class="wb-project-head">
|
|
1676
|
+
<span class="wb-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
|
|
1564
1677
|
<span class="wb-chevron" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1565
1678
|
<span class="wb-project-title">${esc(w.title || wbBaseName(w.path) || w.path)}</span>
|
|
1566
1679
|
<button class="mini-btn wb-new" type="button" data-wb-new="${esc(id)}">${esc(t('wb.newSession'))}</button>
|
|
1567
1680
|
</div>${body}
|
|
1568
1681
|
</div>`
|
|
1569
1682
|
}).join('')
|
|
1683
|
+
if (window.DshMotion?.relayout) {
|
|
1684
|
+
window.DshMotion.relayout(panel, '.wb-project', () => { panel.innerHTML = projectHtml })
|
|
1685
|
+
} else panel.innerHTML = projectHtml
|
|
1686
|
+
window.DshMotion?.list(panel, '.wb-session')
|
|
1687
|
+
window.DshMotion?.bindLongPressReorder(panel, '.wb-project', {
|
|
1688
|
+
handleSelector: '.wb-project-head',
|
|
1689
|
+
excludeSelector: '[data-wb-new]',
|
|
1690
|
+
onCommit: ({ order }) => commitWorkspaceOrder(order)
|
|
1691
|
+
})
|
|
1692
|
+
window.DshMotion?.bindLongPressReorder(panel, '.session-swipe', {
|
|
1693
|
+
groupSelector: '.wb-project',
|
|
1694
|
+
handleSelector: '.wb-session',
|
|
1695
|
+
onCommit: ({ item, order }) => commitWorkspaceSessionOrder(item.closest('[data-wb-project]')?.dataset.wbProject, order)
|
|
1696
|
+
})
|
|
1570
1697
|
}
|
|
1571
1698
|
|
|
1572
1699
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
@@ -1594,6 +1721,16 @@ function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
|
1594
1721
|
if (!sessionId || !Number.isFinite(time)) return
|
|
1595
1722
|
state.sessionTurnTimes[sessionId] = Math.max(Number(state.sessionTurnTimes[sessionId]) || 0, time)
|
|
1596
1723
|
}
|
|
1724
|
+
function sessionWorkspaceOrderKey(s) {
|
|
1725
|
+
const workspace = workspaceForSession(s)
|
|
1726
|
+
return String(workspace?.workspaceId || 'path:' + (sessionWorkspaceLabel(s) || WORKSPACE_UNGROUPED))
|
|
1727
|
+
}
|
|
1728
|
+
function commitWorkspaceGroupOrder(order) {
|
|
1729
|
+
saveWorkbenchOrder(value => { value.workspaceIds = order.map(String) })
|
|
1730
|
+
renderSessions()
|
|
1731
|
+
renderWorkbench()
|
|
1732
|
+
toast(t('wb.orderSaved'), 'ok')
|
|
1733
|
+
}
|
|
1597
1734
|
function sortedSessions() {
|
|
1598
1735
|
const items = topLevelSessions()
|
|
1599
1736
|
if (state.sessionSort === 'workspace') {
|
|
@@ -1618,16 +1755,9 @@ function renderSessions() {
|
|
|
1618
1755
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1619
1756
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1620
1757
|
const showArchived = LS.get('showArchivedV1', '0') === '1'
|
|
1621
|
-
const
|
|
1622
|
-
let lastWorkspace = null
|
|
1623
|
-
const rows = []
|
|
1624
|
-
for (const s of items) {
|
|
1758
|
+
const renderSession = s => {
|
|
1625
1759
|
const workspace = sessionWorkspaceLabel(s)
|
|
1626
1760
|
const workspaceTitle = sessionWorkspaceName(s)
|
|
1627
|
-
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1628
|
-
rows.push(`<div class="session-group-label" title="${esc(workspace)}"><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(workspaceTitle)}</span></div>`)
|
|
1629
|
-
lastWorkspace = workspace
|
|
1630
|
-
}
|
|
1631
1761
|
const title = titleOf(s)
|
|
1632
1762
|
const goal = goalOf(s)
|
|
1633
1763
|
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
@@ -1638,7 +1768,7 @@ function renderSessions() {
|
|
|
1638
1768
|
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
1639
1769
|
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
1640
1770
|
const archiveButton = archivedSet.has(s.sessionId) ? '' : `<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>`
|
|
1641
|
-
|
|
1771
|
+
return `<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1642
1772
|
<div class="session-card ${state.current === s.sessionId ? 'current' : ''}">
|
|
1643
1773
|
<div class="sc-title">${esc(title)}</div>
|
|
1644
1774
|
<div class="sc-meta">
|
|
@@ -1651,13 +1781,42 @@ function renderSessions() {
|
|
|
1651
1781
|
<span class="sc-arrow">›</span>
|
|
1652
1782
|
</div>
|
|
1653
1783
|
${archiveButton}
|
|
1654
|
-
</div>`
|
|
1784
|
+
</div>`
|
|
1785
|
+
}
|
|
1786
|
+
const renderItems = (items) => {
|
|
1787
|
+
if (state.sessionSort !== 'workspace') return items.map(renderSession).join('')
|
|
1788
|
+
const groups = []
|
|
1789
|
+
const byKey = new Map()
|
|
1790
|
+
for (const session of items) {
|
|
1791
|
+
const key = sessionWorkspaceOrderKey(session)
|
|
1792
|
+
let group = byKey.get(key)
|
|
1793
|
+
if (!group) {
|
|
1794
|
+
group = { key, label: sessionWorkspaceName(session), path: sessionWorkspaceLabel(session), items: [] }
|
|
1795
|
+
byKey.set(key, group)
|
|
1796
|
+
groups.push(group)
|
|
1797
|
+
}
|
|
1798
|
+
group.items.push(session)
|
|
1655
1799
|
}
|
|
1656
|
-
|
|
1800
|
+
const { value } = workbenchOrderScopeValue()
|
|
1801
|
+
return orderedItems(groups, value.workspaceIds, group => group.key).map(group => `<div class="session-workspace-group" data-workspace-group="${esc(group.key)}" data-motion-key="${esc(group.key)}">
|
|
1802
|
+
<div class="session-group-label" data-reorder-handle title="${esc(group.path)}"><span class="session-group-drag-handle" aria-hidden="true">⠿</span><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(group.label)}</span></div>
|
|
1803
|
+
${orderedWorkspaceSessions(group.key, group.items).map(renderSession).join('')}
|
|
1804
|
+
</div>`).join('')
|
|
1657
1805
|
}
|
|
1658
1806
|
const divider = archived.length ? `<button class="archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1659
1807
|
const rows = renderItems(main) + divider + (showArchived ? renderItems(archived) : '')
|
|
1660
|
-
list.innerHTML = rows || `<div class="empty">${esc(t('home.empty'))}</div>`
|
|
1808
|
+
const renderList = () => { list.innerHTML = rows || `<div class="empty">${esc(t('home.empty'))}</div>` }
|
|
1809
|
+
if (window.DshMotion?.relayout) window.DshMotion.relayout(list, '.session-swipe', renderList)
|
|
1810
|
+
else renderList()
|
|
1811
|
+
window.DshMotion?.bindLongPressReorder(list, '.session-workspace-group', {
|
|
1812
|
+
handleSelector: '.session-group-label',
|
|
1813
|
+
onCommit: ({ order }) => commitWorkspaceGroupOrder(order)
|
|
1814
|
+
})
|
|
1815
|
+
window.DshMotion?.bindLongPressReorder(list, '.session-swipe', {
|
|
1816
|
+
groupSelector: '.session-workspace-group',
|
|
1817
|
+
handleSelector: '.session-card',
|
|
1818
|
+
onCommit: ({ item, order }) => commitWorkspaceSessionOrder(item.closest('[data-workspace-group]')?.dataset.workspaceGroup, order)
|
|
1819
|
+
})
|
|
1661
1820
|
list.classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1662
1821
|
const sort = $('session-sort')
|
|
1663
1822
|
if (sort) { sort.value = state.sessionSort; syncCustomSelect(sort) }
|
|
@@ -1674,9 +1833,12 @@ function renderSessions() {
|
|
|
1674
1833
|
/* ---------------- 会话详情 ---------------- */
|
|
1675
1834
|
async function openSession(id) {
|
|
1676
1835
|
state.current = id
|
|
1836
|
+
setSessionRecovery('loading')
|
|
1677
1837
|
state.history = emptyHistory()
|
|
1678
1838
|
document.body.classList.add('in-session')
|
|
1679
1839
|
showView('view-session')
|
|
1840
|
+
$('btn-rename-session').classList.remove('hidden')
|
|
1841
|
+
$('btn-archive-session').classList.toggle('hidden', (state.wbArchived || []).includes(id))
|
|
1680
1842
|
$('session-cards').innerHTML = ''
|
|
1681
1843
|
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
1682
1844
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
@@ -1691,6 +1853,9 @@ function closeSession() {
|
|
|
1691
1853
|
setComposerFullscreen(false)
|
|
1692
1854
|
clearComposerImages()
|
|
1693
1855
|
state.current = null
|
|
1856
|
+
setSessionRecovery('idle')
|
|
1857
|
+
$('btn-rename-session').classList.add('hidden')
|
|
1858
|
+
$('btn-archive-session').classList.add('hidden')
|
|
1694
1859
|
state.history = emptyHistory()
|
|
1695
1860
|
document.body.classList.remove('in-session')
|
|
1696
1861
|
hideComposerMenu()
|
|
@@ -1705,7 +1870,7 @@ function bindNativeBack() {
|
|
|
1705
1870
|
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1706
1871
|
if (customSelectCurrent) { closeCustomSelect(); return }
|
|
1707
1872
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
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 } // 先关弹窗
|
|
1873
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else if (openModal.id === 'modal-rename') closeRenameSession(); else if (openModal.id === 'modal-app-version-warning') closeAppVersionWarning(false); else if (openModal.id === 'modal-scan-live') closeLiveScan(''); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1709
1874
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1710
1875
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1711
1876
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1728,6 +1893,8 @@ function renderSessionSub() {
|
|
|
1728
1893
|
if (s.cwd) parts.push(s.cwd)
|
|
1729
1894
|
if (s.running) parts.push(t('session.running'))
|
|
1730
1895
|
else if (s.error) parts.push(t('session.interrupted'))
|
|
1896
|
+
const recovery = recoveryLabel()
|
|
1897
|
+
if (recovery) parts.push(recovery)
|
|
1731
1898
|
$('session-sub').textContent = parts.join(' · ')
|
|
1732
1899
|
}
|
|
1733
1900
|
|
|
@@ -1867,6 +2034,7 @@ function restoreCachedHistory() {
|
|
|
1867
2034
|
if (!id) return false
|
|
1868
2035
|
const cached = readHistoryCache()[id]
|
|
1869
2036
|
if (!cached?.events?.length) return false
|
|
2037
|
+
if (cached.title) hydrateSessionProjections(id, { values: { title: cached.title }, asOfSeq: 0 })
|
|
1870
2038
|
const h = emptyHistory()
|
|
1871
2039
|
for (const e of cached.events) {
|
|
1872
2040
|
if (e?.seq == null) continue
|
|
@@ -1884,6 +2052,7 @@ async function loadHistory(reset) {
|
|
|
1884
2052
|
const id = state.current
|
|
1885
2053
|
if (!id || state.history.loading) return
|
|
1886
2054
|
state.history.loading = true
|
|
2055
|
+
if (reset) setSessionRecovery('loading')
|
|
1887
2056
|
const moreBtn = $('history-more')
|
|
1888
2057
|
if (moreBtn) moreBtn.classList.add('hidden')
|
|
1889
2058
|
const payload = { sessionId: id, maxMessages: 60 }
|
|
@@ -1896,10 +2065,12 @@ async function loadHistory(reset) {
|
|
|
1896
2065
|
state.history.loading = false
|
|
1897
2066
|
if (e.message === 'AUTH') { authFailure(); return }
|
|
1898
2067
|
if (restoreCachedHistory()) {
|
|
2068
|
+
setSessionRecovery('cached', e.message)
|
|
1899
2069
|
toast(t('history.cacheFallback'), 'ok')
|
|
1900
2070
|
return
|
|
1901
2071
|
}
|
|
1902
2072
|
const msg = e.message || t('err.dshError')
|
|
2073
|
+
setSessionRecovery('error', msg)
|
|
1903
2074
|
const box = $('history')
|
|
1904
2075
|
if (box && (reset || !state.history.visible.length)) {
|
|
1905
2076
|
box.innerHTML = `<div class="empty"><div>${esc(t('history.loadFailed', { msg }))}</div><button type="button" class="mini-btn" id="btn-history-retry" style="margin-top:10px">${esc(t('history.retry'))}</button></div>`
|
|
@@ -1911,6 +2082,7 @@ async function loadHistory(reset) {
|
|
|
1911
2082
|
return
|
|
1912
2083
|
}
|
|
1913
2084
|
|
|
2085
|
+
hydrateSessionProjections(id, v.projections)
|
|
1914
2086
|
const incoming = v.events || []
|
|
1915
2087
|
let added = 0
|
|
1916
2088
|
if (reset) state.history.partialReasoning.clear()
|
|
@@ -1932,6 +2104,8 @@ async function loadHistory(reset) {
|
|
|
1932
2104
|
trimVisible()
|
|
1933
2105
|
state.history.hasMore = !!v.hasMore
|
|
1934
2106
|
state.history.loading = false
|
|
2107
|
+
setSessionRecovery('ready')
|
|
2108
|
+
renderSessionTitle(); renderSessionSub(); renderSessionCards()
|
|
1935
2109
|
try {
|
|
1936
2110
|
if (reset) renderHistory(true)
|
|
1937
2111
|
else if (added) renderHistory(false, 'keep')
|
|
@@ -2249,10 +2423,15 @@ async function renderSessionCards() {
|
|
|
2249
2423
|
const running = e.activity === 'running'
|
|
2250
2424
|
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>`
|
|
2251
2425
|
}).join('')
|
|
2252
|
-
|
|
2426
|
+
const subagentClosedIcon = 'M7 10l5 5 5-5'
|
|
2427
|
+
const subagentOpenIcon = 'M7 14l5-5 5 5'
|
|
2428
|
+
const subagentIcon = expanded ? subagentOpenIcon : subagentClosedIcon
|
|
2429
|
+
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"><morph-icon data-morph-state="${expanded ? 'open' : 'closed'}" data-morph-closed="${subagentClosedIcon}" data-morph-open="${subagentOpenIcon}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${subagentIcon}"/></svg></morph-icon></span></button><div class="subagent-list${expanded ? '' : ' hidden'}">${rows}</div></div>`)
|
|
2253
2430
|
box.querySelector('[data-subagent-toggle]')?.addEventListener('click', () => {
|
|
2431
|
+
const icon = box.querySelector('[data-subagent-toggle] morph-icon')
|
|
2432
|
+
if (icon) icon.setAttribute('data-morph-state', expanded ? 'closed' : 'open')
|
|
2254
2433
|
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
2255
|
-
renderSessionCards()
|
|
2434
|
+
setTimeout(() => renderSessionCards(), 240)
|
|
2256
2435
|
})
|
|
2257
2436
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
2258
2437
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
@@ -2428,20 +2607,24 @@ async function sendSessionContent(text, images) {
|
|
|
2428
2607
|
try {
|
|
2429
2608
|
const content = [...await encodeComposerImagesFor(images)]
|
|
2430
2609
|
if (clean) content.push({ type: 'text', text: clean })
|
|
2610
|
+
setSessionRecovery('resuming')
|
|
2431
2611
|
const v = await safeRpc('session.prompt', {
|
|
2432
2612
|
sessionId: state.current,
|
|
2433
2613
|
mode: 'queue',
|
|
2434
2614
|
content
|
|
2435
2615
|
}, t('send.failed'))
|
|
2436
2616
|
if (v?.accepted) {
|
|
2617
|
+
setSessionRecovery('ready')
|
|
2437
2618
|
noteSessionTurnTime(state.current, Date.now())
|
|
2438
2619
|
renderSessions()
|
|
2439
2620
|
toast(images.length ? t('send.imageSent') : (clean.startsWith('/') ? t('send.commandSent') : t('send.sent')), 'ok')
|
|
2440
2621
|
return true
|
|
2441
2622
|
}
|
|
2442
2623
|
if (v?.command?.text) { toast(t('send.commandExecuted'), 'ok'); return true }
|
|
2624
|
+
setSessionRecovery('error')
|
|
2443
2625
|
return false
|
|
2444
2626
|
} catch (e) {
|
|
2627
|
+
setSessionRecovery('error', e?.message)
|
|
2445
2628
|
toast(t('composer.imageReadFailed', { msg: e?.message || e }), 'err')
|
|
2446
2629
|
return false
|
|
2447
2630
|
} finally {
|
|
@@ -2603,7 +2786,41 @@ async function cancelSession() {
|
|
|
2603
2786
|
if (!state.current) return
|
|
2604
2787
|
if (!confirm(t('session.confirmStop'))) return
|
|
2605
2788
|
const v = await safeRpc('session.cancel', { sessionId: state.current }, t('session.stopFailed'))
|
|
2606
|
-
if (v?.accepted) toast(t('session.stopRequested'), 'ok')
|
|
2789
|
+
if (v?.accepted) { setSessionRecovery('ready'); toast(t('session.stopRequested'), 'ok') }
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
let renamePendingSessionId = null
|
|
2793
|
+
function renameSession(sessionId = state.current) {
|
|
2794
|
+
const session = state.byId.get(sessionId)
|
|
2795
|
+
if (!session) return
|
|
2796
|
+
renamePendingSessionId = sessionId
|
|
2797
|
+
$('rename-session-input').value = titleOf(session)
|
|
2798
|
+
$('modal-rename').classList.remove('hidden')
|
|
2799
|
+
setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
|
|
2800
|
+
}
|
|
2801
|
+
function closeRenameSession() {
|
|
2802
|
+
renamePendingSessionId = null
|
|
2803
|
+
$('modal-rename').classList.add('hidden')
|
|
2804
|
+
}
|
|
2805
|
+
async function confirmRenameSession() {
|
|
2806
|
+
const sessionId = renamePendingSessionId
|
|
2807
|
+
if (!sessionId) return
|
|
2808
|
+
const title = $('rename-session-input').value.trim()
|
|
2809
|
+
if (!title) return toast(t('session.renameEmpty'), 'err')
|
|
2810
|
+
const button = $('rename-confirm')
|
|
2811
|
+
button.disabled = true
|
|
2812
|
+
setSessionRecovery('resuming')
|
|
2813
|
+
try {
|
|
2814
|
+
const value = await safeRpc('session.rename', { sessionId, title }, t('session.renameFailed'))
|
|
2815
|
+
if (value == null) { setSessionRecovery('error'); return }
|
|
2816
|
+
if (value.title) applyProjection(sessionId, 'title', value.title, value.seq)
|
|
2817
|
+
closeRenameSession()
|
|
2818
|
+
setSessionRecovery('ready')
|
|
2819
|
+
toast(t('session.renamed'), 'ok')
|
|
2820
|
+
await refreshSessions()
|
|
2821
|
+
} finally {
|
|
2822
|
+
button.disabled = false
|
|
2823
|
+
}
|
|
2607
2824
|
}
|
|
2608
2825
|
|
|
2609
2826
|
async function newSession() {
|
|
@@ -2673,6 +2890,7 @@ async function confirmArchiveSession() {
|
|
|
2673
2890
|
closeArchiveConfirm()
|
|
2674
2891
|
toast(t('session.archived'), 'ok')
|
|
2675
2892
|
await refreshSessions()
|
|
2893
|
+
if (state.current === sessionId) closeSession()
|
|
2676
2894
|
} finally {
|
|
2677
2895
|
button.disabled = false
|
|
2678
2896
|
}
|
|
@@ -3346,6 +3564,8 @@ async function runFsUpload(up) {
|
|
|
3346
3564
|
xhr.setRequestHeader('authorization', 'Bearer ' + state.token)
|
|
3347
3565
|
xhr.setRequestHeader('x-dsh-remote-client', CAP?.isNativePlatform?.() ? 'app' : 'web')
|
|
3348
3566
|
if (CLIENT_ID) xhr.setRequestHeader('x-dsh-remote-client-id', CLIENT_ID)
|
|
3567
|
+
if (params.offset != null) xhr.setRequestHeader('Upload-Offset', String(params.offset))
|
|
3568
|
+
if (params.size != null) xhr.setRequestHeader('Upload-Length', String(params.size))
|
|
3349
3569
|
xhr.upload.onprogress = (e) => {
|
|
3350
3570
|
if (e.lengthComputable) {
|
|
3351
3571
|
const loaded = up.offset + Math.min(e.loaded, e.total)
|
|
@@ -3370,7 +3590,7 @@ async function runFsUpload(up) {
|
|
|
3370
3590
|
})
|
|
3371
3591
|
|
|
3372
3592
|
const probe = async () => {
|
|
3373
|
-
const res = await fetch(fsApiUrl('/upload-probe', { path: up.path, name: up.name, session: up.session }), { headers: fsHeaders() })
|
|
3593
|
+
const res = await fetch(fsApiUrl('/upload-probe', { path: up.path, name: up.name, session: up.session, size: String(up.size) }), { headers: fsHeaders() })
|
|
3374
3594
|
if (res.status === 401) { fsAuthError(401); return null }
|
|
3375
3595
|
const json = await res.json().catch(() => ({}))
|
|
3376
3596
|
if (json.ok) up.offset = json.partialSize || 0
|
|
@@ -3402,7 +3622,7 @@ async function runFsUpload(up) {
|
|
|
3402
3622
|
}
|
|
3403
3623
|
hasher.update(chunkBytes)
|
|
3404
3624
|
const isLast = end >= up.size
|
|
3405
|
-
const params = { path: up.path, name: up.name, session: up.session, offset: String(up.offset) }
|
|
3625
|
+
const params = { path: up.path, name: up.name, session: up.session, offset: String(up.offset), size: String(up.size) }
|
|
3406
3626
|
if (isLast) { params.finish = '1'; params.sha256 = hasher.hex() }
|
|
3407
3627
|
if (overwrite) params.overwrite = '1'
|
|
3408
3628
|
const r = await uploadChunk(params, blob)
|
|
@@ -3444,7 +3664,7 @@ async function runFsUpload(up) {
|
|
|
3444
3664
|
// 发一个空 finish 块完成收尾, 同时带上全量 SHA-256 校验
|
|
3445
3665
|
if (up.offset >= up.size) {
|
|
3446
3666
|
const expected = hasher.hex()
|
|
3447
|
-
const params = { path: up.path, name: up.name, session: up.session, offset: String(up.offset), finish: '1', sha256: expected }
|
|
3667
|
+
const params = { path: up.path, name: up.name, session: up.session, offset: String(up.offset), size: String(up.size), finish: '1', sha256: expected }
|
|
3448
3668
|
if (overwrite) params.overwrite = '1'
|
|
3449
3669
|
const r = await uploadChunk(params, new Blob([]))
|
|
3450
3670
|
if (r.status === 401) { fsAuthError(401); return }
|
|
@@ -4264,7 +4484,7 @@ function saveBgConfig(enabled) {
|
|
|
4264
4484
|
const b = bgBridge()
|
|
4265
4485
|
if (!b?.saveBackgroundConfig) return false
|
|
4266
4486
|
const base = bgBase()
|
|
4267
|
-
const intervalMin = parseFloat($('bg-interval')?.value || '
|
|
4487
|
+
const intervalMin = parseFloat($('bg-interval')?.value || '0.5') || 0.5
|
|
4268
4488
|
const notifyTaskDone = $('opt-task-done')?.checked !== false
|
|
4269
4489
|
b.saveBackgroundConfig(JSON.stringify({ enabled, intervalMin, base, token: state.token || '', clientId: CLIENT_ID || '', notifyTaskDone }))
|
|
4270
4490
|
if (enabled) $('bg-auth-status')?.classList.add('hidden')
|
|
@@ -4397,6 +4617,7 @@ function showView(id) {
|
|
|
4397
4617
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
4398
4618
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
4399
4619
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
4620
|
+
window.DshMotion?.view($(id))
|
|
4400
4621
|
window.scrollTo(0, 0)
|
|
4401
4622
|
if (id === 'view-files' && !state.fs.loaded) {
|
|
4402
4623
|
const workspace = workspaceById(state.fs.workspaceId)
|
|
@@ -4485,8 +4706,7 @@ function updateComposerFullscreenButton() {
|
|
|
4485
4706
|
const shouldShow = active || input.scrollHeight > 120
|
|
4486
4707
|
button.classList.toggle('hidden', !shouldShow)
|
|
4487
4708
|
$('composer-input-wrap')?.classList.toggle('has-fs-btn', shouldShow)
|
|
4488
|
-
$('fs-ico
|
|
4489
|
-
$('fs-ico-collapse')?.classList.toggle('hidden', !active)
|
|
4709
|
+
$('fs-ico')?.setAttribute('data-morph-state', active ? 'open' : 'closed')
|
|
4490
4710
|
button.title = t(active ? 'composer.exitFullscreen' : 'composer.fullscreen')
|
|
4491
4711
|
button.setAttribute('aria-label', t(active ? 'composer.exitFullscreen' : 'composer.fullscreen'))
|
|
4492
4712
|
}
|
|
@@ -5275,6 +5495,12 @@ function bindUi() {
|
|
|
5275
5495
|
})
|
|
5276
5496
|
$('modal-file-preview').addEventListener('click', (e) => { if (e.target === $('modal-file-preview')) closeFsPreview() })
|
|
5277
5497
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
5498
|
+
$('btn-rename-session').addEventListener('click', () => renameSession())
|
|
5499
|
+
$('btn-archive-session').addEventListener('click', () => archiveSession(state.current))
|
|
5500
|
+
$('rename-cancel').addEventListener('click', closeRenameSession)
|
|
5501
|
+
$('rename-confirm').addEventListener('click', confirmRenameSession)
|
|
5502
|
+
$('rename-session-input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) confirmRenameSession() })
|
|
5503
|
+
$('modal-rename').addEventListener('click', e => { if (e.target === $('modal-rename')) closeRenameSession() })
|
|
5278
5504
|
$('btn-send').addEventListener('click', sendMessage)
|
|
5279
5505
|
$('btn-fs-send').addEventListener('click', sendMessage)
|
|
5280
5506
|
$('btn-plus').addEventListener('click', toggleComposerMenu)
|