dsh-remote-plugin 0.6.14 → 0.6.16
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 +314 -30
- package/package.json +1 -1
- package/public/admin.html +46 -1
- package/public/admin.js +79 -9
- package/public/announcements.json +33 -0
- package/public/app.js +1103 -62
- 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 +120 -15
- 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 +96 -6
- package/public/update.json +12 -12
- 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
|
@@ -69,6 +69,9 @@ const state = {
|
|
|
69
69
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
70
70
|
byId: new Map(),
|
|
71
71
|
current: null,
|
|
72
|
+
sessionRecovery: { status: 'idle', error: '' },
|
|
73
|
+
pendingProjections: new Map(),
|
|
74
|
+
lastStreamResyncAt: 0,
|
|
72
75
|
hostInfo: null,
|
|
73
76
|
history: emptyDesktopHistory(),
|
|
74
77
|
approvals: [],
|
|
@@ -1017,6 +1020,7 @@ function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
|
1017
1020
|
updateConn()
|
|
1018
1021
|
if (kind === 'mux') { state.approvals = []; state.questions = []; renderNotifStack() }
|
|
1019
1022
|
if (refreshOnOpen) refreshSessions()
|
|
1023
|
+
if (allStreamsOpen()) resyncAfterStreamOpen()
|
|
1020
1024
|
}
|
|
1021
1025
|
ws.onmessage = (msg) => {
|
|
1022
1026
|
if (!streamIsCurrent(kind, ws, generation)) return
|
|
@@ -1113,6 +1117,7 @@ async function pollKind(kind) {
|
|
|
1113
1117
|
state.pollSeq[kind] = 0
|
|
1114
1118
|
if (kind === 'mux') renderNotifStack()
|
|
1115
1119
|
refreshSessions()
|
|
1120
|
+
if (state.current) void resyncCurrentSession()
|
|
1116
1121
|
}
|
|
1117
1122
|
for (const item of data.events) {
|
|
1118
1123
|
if (item.seq > (state.pollSeq[kind] || 0)) {
|
|
@@ -1183,20 +1188,67 @@ function onHostFrame(full) {
|
|
|
1183
1188
|
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
1184
1189
|
}
|
|
1185
1190
|
}
|
|
1191
|
+
function hydrateSessionProjections(sessionId, projections) {
|
|
1192
|
+
const s = state.byId.get(sessionId)
|
|
1193
|
+
if (!s || !projections || typeof projections !== 'object') return
|
|
1194
|
+
const incomingSeq = Number(projections.asOfSeq) || 0
|
|
1195
|
+
const current = s.projections || { asOfSeq: 0, values: {} }
|
|
1196
|
+
const currentSeq = Number(current.asOfSeq) || 0
|
|
1197
|
+
if (incomingSeq < currentSeq) return
|
|
1198
|
+
s.projections = {
|
|
1199
|
+
asOfSeq: Math.max(currentSeq, incomingSeq),
|
|
1200
|
+
values: { ...(current.values || {}), ...(projections.values || {}) }
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function applyPendingProjections() {
|
|
1204
|
+
for (const [sessionId, projections] of state.pendingProjections) {
|
|
1205
|
+
if (!state.byId.has(sessionId)) continue
|
|
1206
|
+
hydrateSessionProjections(sessionId, projections)
|
|
1207
|
+
state.pendingProjections.delete(sessionId)
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1186
1210
|
function applyProjection(sessionId, key, value, seq) {
|
|
1187
1211
|
const s = state.byId.get(sessionId)
|
|
1188
|
-
if (s) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1212
|
+
if (!s) {
|
|
1213
|
+
const pending = state.pendingProjections.get(sessionId) || { asOfSeq: 0, values: {} }
|
|
1214
|
+
pending.values[key] = value
|
|
1215
|
+
pending.asOfSeq = Math.max(pending.asOfSeq || 0, seq || 0)
|
|
1216
|
+
state.pendingProjections.set(sessionId, pending)
|
|
1217
|
+
return
|
|
1193
1218
|
}
|
|
1219
|
+
const currentSeq = Number(s.projections?.asOfSeq) || 0
|
|
1220
|
+
if (seq && seq < currentSeq) return
|
|
1221
|
+
s.projections = s.projections || { asOfSeq: 0, values: {} }
|
|
1222
|
+
s.projections.values = s.projections.values || {}
|
|
1223
|
+
s.projections.values[key] = value
|
|
1224
|
+
s.projections.asOfSeq = Math.max(currentSeq, seq || 0)
|
|
1194
1225
|
if (state.current === sessionId) {
|
|
1195
1226
|
renderSessions()
|
|
1196
1227
|
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
1197
1228
|
}
|
|
1198
1229
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
1199
1230
|
}
|
|
1231
|
+
function setSessionRecovery(status, error = '') {
|
|
1232
|
+
state.sessionRecovery = { status, error: String(error || '') }
|
|
1233
|
+
updateSessionActions()
|
|
1234
|
+
}
|
|
1235
|
+
function recoveryLabel() {
|
|
1236
|
+
const status = state.sessionRecovery.status
|
|
1237
|
+
if (status === 'loading' || status === 'resuming') return t('ds.sessionRecovering')
|
|
1238
|
+
if (status === 'error') return t('ds.sessionRecoveryFailed')
|
|
1239
|
+
return ''
|
|
1240
|
+
}
|
|
1241
|
+
function resyncCurrentSession() {
|
|
1242
|
+
if (!state.current) return Promise.resolve()
|
|
1243
|
+
return loadHistory().then(() => {
|
|
1244
|
+
if (state.current) { renderSessionCards(); updateComposerStatus(); updateSessionActions() }
|
|
1245
|
+
})
|
|
1246
|
+
}
|
|
1247
|
+
function resyncAfterStreamOpen() {
|
|
1248
|
+
if (!state.current || Date.now() - state.lastStreamResyncAt < 1200) return
|
|
1249
|
+
state.lastStreamResyncAt = Date.now()
|
|
1250
|
+
void refreshSessions().then(() => resyncCurrentSession())
|
|
1251
|
+
}
|
|
1200
1252
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1201
1253
|
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
|
|
1202
1254
|
function isTopLevelSession(session) {
|
|
@@ -1246,6 +1298,7 @@ async function refreshSessions() {
|
|
|
1246
1298
|
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1247
1299
|
state.sessions = v.items || []
|
|
1248
1300
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1301
|
+
applyPendingProjections()
|
|
1249
1302
|
renderSessions()
|
|
1250
1303
|
scheduleWorkbenchRefresh()
|
|
1251
1304
|
renderOverviewDesktop()
|
|
@@ -1258,6 +1311,14 @@ function sessionWorkspaceLabel(s) {
|
|
|
1258
1311
|
function sessionSortTime(s) {
|
|
1259
1312
|
return Math.max(Number(state.sessionTurnTimes[s?.sessionId]) || 0, Number(s?.updatedAt) || 0, Number(s?.createdAt) || 0)
|
|
1260
1313
|
}
|
|
1314
|
+
function sessionWorkspaceOrderKey(s) {
|
|
1315
|
+
return 'path:' + (sessionWorkspaceLabel(s) || 'workspace-unknown')
|
|
1316
|
+
}
|
|
1317
|
+
function commitWorkspaceGroupOrder(order) {
|
|
1318
|
+
saveWorkbenchOrder(value => { value.workspaceIds = order.map(String) })
|
|
1319
|
+
renderSessions()
|
|
1320
|
+
toast(t('ds.wbOrderSaved'), 'ok')
|
|
1321
|
+
}
|
|
1261
1322
|
function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
1262
1323
|
const raw = typeof eventOrTime === 'object' ? eventOrTime?.time : eventOrTime
|
|
1263
1324
|
const time = Number(raw) > 0 ? Number(raw) : Date.now()
|
|
@@ -1297,30 +1358,54 @@ function renderSessions() {
|
|
|
1297
1358
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1298
1359
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1299
1360
|
const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
|
|
1300
|
-
const
|
|
1301
|
-
let lastWorkspace = null
|
|
1302
|
-
const rows = []
|
|
1303
|
-
for (const s of items) {
|
|
1361
|
+
const renderSession = s => {
|
|
1304
1362
|
const workspace = sessionWorkspaceLabel(s)
|
|
1305
1363
|
const workspaceName = workspaceDisplayName(workspace)
|
|
1306
|
-
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1307
|
-
rows.push(`<div class="ds-session-group" title="${esc(workspace)}"><span class="ds-session-group-icon" aria-hidden="true">⌂</span><span class="ds-session-group-name">${esc(workspaceName)}</span></div>`)
|
|
1308
|
-
lastWorkspace = workspace
|
|
1309
|
-
}
|
|
1310
1364
|
const title = titleOf(s)
|
|
1311
|
-
|
|
1365
|
+
return `<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1312
1366
|
<span class="ds-session-title">${esc(title)}</span>
|
|
1313
1367
|
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1314
1368
|
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(sessionSortTime(s))}</span>
|
|
1315
|
-
</button>`
|
|
1369
|
+
</button>`
|
|
1370
|
+
}
|
|
1371
|
+
const renderItems = (items) => {
|
|
1372
|
+
if (state.sessionSort !== 'workspace') return items.map(renderSession).join('')
|
|
1373
|
+
const groups = []
|
|
1374
|
+
const byKey = new Map()
|
|
1375
|
+
for (const session of items) {
|
|
1376
|
+
const key = sessionWorkspaceOrderKey(session)
|
|
1377
|
+
let group = byKey.get(key)
|
|
1378
|
+
if (!group) {
|
|
1379
|
+
group = { key, label: workspaceDisplayName(sessionWorkspaceLabel(session)), path: sessionWorkspaceLabel(session), items: [] }
|
|
1380
|
+
byKey.set(key, group)
|
|
1381
|
+
groups.push(group)
|
|
1382
|
+
}
|
|
1383
|
+
group.items.push(session)
|
|
1316
1384
|
}
|
|
1317
|
-
|
|
1385
|
+
const { value } = workbenchOrderScopeValue()
|
|
1386
|
+
return orderedItems(groups, value.workspaceIds, group => group.key).map(group => `<div class="ds-session-workspace-group" data-workspace-group="${esc(group.key)}" data-motion-key="${esc(group.key)}">
|
|
1387
|
+
<div class="ds-session-group" data-reorder-handle title="${esc(group.path)}"><span class="ds-session-group-drag-handle" aria-hidden="true">⠿</span><span class="ds-session-group-icon" aria-hidden="true">⌂</span><span class="ds-session-group-name">${esc(group.label)}</span></div>
|
|
1388
|
+
${orderedWorkspaceSessions(group.key, group.items).map(renderSession).join('')}
|
|
1389
|
+
</div>`).join('')
|
|
1318
1390
|
}
|
|
1319
1391
|
const divider = archived.length ? `<button class="ds-archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1320
1392
|
const hiddenByWorkbench = allItems.length - visible.length
|
|
1321
1393
|
const html = renderItems(main) + divider + (showArchived ? renderItems(archived) : '') || `<div class="ds-empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('ds.sessionsEmpty'))}</div>`
|
|
1322
1394
|
$('session-list').innerHTML = html
|
|
1323
1395
|
$('mobile-session-list').innerHTML = html
|
|
1396
|
+
window.DshMotion?.list($('session-list'), '.ds-session-item')
|
|
1397
|
+
window.DshMotion?.list($('mobile-session-list'), '.ds-session-item')
|
|
1398
|
+
for (const list of [$('session-list'), $('mobile-session-list')].filter(Boolean)) {
|
|
1399
|
+
window.DshMotion?.bindLongPressReorder(list, '.ds-session-workspace-group', {
|
|
1400
|
+
handleSelector: '.ds-session-group',
|
|
1401
|
+
onCommit: ({ order }) => commitWorkspaceGroupOrder(order)
|
|
1402
|
+
})
|
|
1403
|
+
window.DshMotion?.bindLongPressReorder(list, '.ds-session-item', {
|
|
1404
|
+
groupSelector: '.ds-session-workspace-group',
|
|
1405
|
+
handleSelector: '.ds-session-item',
|
|
1406
|
+
onCommit: ({ item, order }) => commitWorkspaceSessionOrder(item.closest('[data-workspace-group]')?.dataset.workspaceGroup, order)
|
|
1407
|
+
})
|
|
1408
|
+
}
|
|
1324
1409
|
$('session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1325
1410
|
$('mobile-session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1326
1411
|
const sort = $('session-sort')
|
|
@@ -1335,10 +1420,12 @@ function renderSessions() {
|
|
|
1335
1420
|
|
|
1336
1421
|
async function openSession(id) {
|
|
1337
1422
|
state.current = id
|
|
1423
|
+
setSessionRecovery('loading')
|
|
1338
1424
|
state.history = emptyDesktopHistory()
|
|
1339
1425
|
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
1340
1426
|
showView('view-chat')
|
|
1341
1427
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
1428
|
+
updateSessionActions()
|
|
1342
1429
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
1343
1430
|
renderSessions()
|
|
1344
1431
|
renderSessionCards()
|
|
@@ -1348,25 +1435,30 @@ async function openSession(id) {
|
|
|
1348
1435
|
}
|
|
1349
1436
|
function closeSession() {
|
|
1350
1437
|
state.current = null
|
|
1438
|
+
setSessionRecovery('idle')
|
|
1351
1439
|
state.history = emptyDesktopHistory()
|
|
1352
1440
|
const cards = $('session-cards')
|
|
1353
1441
|
if (cards) cards.innerHTML = ''
|
|
1354
1442
|
renderQueue()
|
|
1355
1443
|
updateComposerStatus()
|
|
1444
|
+
updateSessionActions()
|
|
1356
1445
|
showView('view-sessions')
|
|
1357
1446
|
}
|
|
1358
1447
|
async function loadHistory() {
|
|
1359
1448
|
const id = state.current
|
|
1360
1449
|
if (!id || state.history.loading) return
|
|
1361
1450
|
state.history.loading = true
|
|
1451
|
+
setSessionRecovery('loading')
|
|
1362
1452
|
let v
|
|
1363
1453
|
try { v = await rpc('session.history', { sessionId: id, maxMessages: 60 }) }
|
|
1364
1454
|
catch (e) {
|
|
1365
1455
|
state.history.loading = false
|
|
1366
1456
|
if (e.message === 'AUTH') return
|
|
1457
|
+
setSessionRecovery('error', e.message)
|
|
1367
1458
|
$('history').innerHTML = `<div class="ds-empty">${e.message}</div>`
|
|
1368
1459
|
return
|
|
1369
1460
|
}
|
|
1461
|
+
hydrateSessionProjections(id, v.projections)
|
|
1370
1462
|
for (const entry of v.events || []) {
|
|
1371
1463
|
const ev = entry?.event
|
|
1372
1464
|
const seq = ev?.seq
|
|
@@ -1380,6 +1472,8 @@ async function loadHistory() {
|
|
|
1380
1472
|
state.history.visible.sort((a, b) => a.seq - b.seq)
|
|
1381
1473
|
state.history.hasMore = !!v.hasMore
|
|
1382
1474
|
state.history.loading = false
|
|
1475
|
+
setSessionRecovery('ready')
|
|
1476
|
+
updateSessionActions()
|
|
1383
1477
|
renderHistory()
|
|
1384
1478
|
}
|
|
1385
1479
|
|
|
@@ -1557,10 +1651,15 @@ async function renderSessionCards() {
|
|
|
1557
1651
|
const running = e.activity === 'running'
|
|
1558
1652
|
return `<div class="ds-card-row"><span class="ds-card-k">${running ? '▶ ' : ''}${esc(label)}</span><span class="ds-card-v">${esc(e.mode)} ${running ? t('subagent.running') : ''}${e.mode === 'continuable' && running ? ` <button class="ds-mini-btn" data-sub-interrupt="${esc(e.id)}">${t('subagent.interrupt')}</button>` : ''}</span></div>`
|
|
1559
1653
|
}).join('')
|
|
1560
|
-
|
|
1654
|
+
const subagentClosedIcon = 'M7 10l5 5 5-5'
|
|
1655
|
+
const subagentOpenIcon = 'M7 14l5-5 5 5'
|
|
1656
|
+
const subagentIcon = expanded ? subagentOpenIcon : subagentClosedIcon
|
|
1657
|
+
box.insertAdjacentHTML('beforeend', `<div class="ds-card ds-subagent-card"><button type="button" class="ds-subagent-toggle" data-subagent-toggle aria-expanded="${expanded}" aria-label="${esc(toggleLabel)}" title="${esc(toggleLabel)}"><span class="ds-card-title">${esc(t('subagent.count', { n: sub.entries.length }))}</span><span class="ds-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="ds-subagent-list${expanded ? '' : ' hidden'}">${rows}</div></div>`)
|
|
1561
1658
|
box.querySelector('[data-subagent-toggle]')?.addEventListener('click', () => {
|
|
1659
|
+
const icon = box.querySelector('[data-subagent-toggle] morph-icon')
|
|
1660
|
+
if (icon) icon.setAttribute('data-morph-state', expanded ? 'closed' : 'open')
|
|
1562
1661
|
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
1563
|
-
renderSessionCards()
|
|
1662
|
+
setTimeout(() => renderSessionCards(), 240)
|
|
1564
1663
|
})
|
|
1565
1664
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1566
1665
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
@@ -1645,18 +1744,73 @@ async function sendMessage() {
|
|
|
1645
1744
|
if (!text || !state.current) return
|
|
1646
1745
|
if (await runSlashCommand(text)) { input.value = ''; return }
|
|
1647
1746
|
input.value = ''
|
|
1747
|
+
setSessionRecovery('resuming')
|
|
1648
1748
|
const v = await safeRpc('session.prompt', {
|
|
1649
1749
|
sessionId: state.current,
|
|
1650
1750
|
mode: 'queue',
|
|
1651
1751
|
content: [{ type: 'text', text }]
|
|
1652
1752
|
}, '')
|
|
1653
|
-
if (v) { noteSessionTurnTime(state.current, Date.now()); renderSessions(); toast(t('ds.toastSent'), 'ok') }
|
|
1753
|
+
if (v) { setSessionRecovery('ready'); noteSessionTurnTime(state.current, Date.now()); renderSessions(); toast(t('ds.toastSent'), 'ok') }
|
|
1754
|
+
else setSessionRecovery('error')
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
async function cancelSession() {
|
|
1758
|
+
if (!state.current) return
|
|
1759
|
+
if (!confirm(t('ds.sessionStopConfirm'))) return
|
|
1760
|
+
const v = await safeRpc('session.cancel', { sessionId: state.current }, t('ds.sessionStopFailed'))
|
|
1761
|
+
if (v?.accepted) { setSessionRecovery('ready'); toast(t('ds.sessionStopRequested'), 'ok') }
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
let renamePendingSessionId = null
|
|
1765
|
+
function renameSession(sessionId = state.current) {
|
|
1766
|
+
const session = state.byId.get(sessionId)
|
|
1767
|
+
if (!session) return
|
|
1768
|
+
renamePendingSessionId = sessionId
|
|
1769
|
+
$('rename-session-input').value = titleOf(session)
|
|
1770
|
+
$('modal-rename').classList.remove('hidden')
|
|
1771
|
+
setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
|
|
1772
|
+
}
|
|
1773
|
+
function closeRenameSession() {
|
|
1774
|
+
renamePendingSessionId = null
|
|
1775
|
+
$('modal-rename').classList.add('hidden')
|
|
1776
|
+
}
|
|
1777
|
+
async function confirmRenameSession() {
|
|
1778
|
+
const sessionId = renamePendingSessionId
|
|
1779
|
+
if (!sessionId) return
|
|
1780
|
+
const title = $('rename-session-input').value.trim()
|
|
1781
|
+
if (!title) return toast(t('ds.sessionRenameEmpty'), 'err')
|
|
1782
|
+
const button = $('rename-confirm')
|
|
1783
|
+
button.disabled = true
|
|
1784
|
+
setSessionRecovery('resuming')
|
|
1785
|
+
try {
|
|
1786
|
+
const value = await safeRpc('session.rename', { sessionId, title }, t('ds.sessionRenameFailed'))
|
|
1787
|
+
if (value == null) { setSessionRecovery('error'); return }
|
|
1788
|
+
if (value.title) applyProjection(sessionId, 'title', value.title, value.seq)
|
|
1789
|
+
closeRenameSession()
|
|
1790
|
+
setSessionRecovery('ready')
|
|
1791
|
+
toast(t('ds.sessionRenamed'), 'ok')
|
|
1792
|
+
await refreshSessions()
|
|
1793
|
+
} finally {
|
|
1794
|
+
button.disabled = false
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
async function archiveCurrentSession() {
|
|
1799
|
+
const sessionId = state.current
|
|
1800
|
+
if (!sessionId || !confirm(t('ds.sessionArchiveConfirm'))) return
|
|
1801
|
+
const value = await safeRpc('workspace.archiveSession', { sessionId }, t('ds.toastOpFailed'))
|
|
1802
|
+
if (!value) return
|
|
1803
|
+
if (Array.isArray(value.archivedSessionIds)) state.archivedIds = value.archivedSessionIds
|
|
1804
|
+
toast(t('ds.sessionArchived'), 'ok')
|
|
1805
|
+
closeSession()
|
|
1806
|
+
await refreshSessions()
|
|
1654
1807
|
}
|
|
1655
1808
|
|
|
1656
1809
|
function updateComposerStatus() {
|
|
1657
1810
|
const status = $('composer-status')
|
|
1658
1811
|
if (!status) return
|
|
1659
1812
|
status.classList.toggle('hidden', !state.byId.get(state.current)?.running)
|
|
1813
|
+
updateSessionActions()
|
|
1660
1814
|
}
|
|
1661
1815
|
function queuePreview(item) {
|
|
1662
1816
|
const blocks = item?.message?.content || item?.content || []
|
|
@@ -1948,6 +2102,64 @@ async function wbGateway(method, pathname, body) {
|
|
|
1948
2102
|
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1949
2103
|
return data
|
|
1950
2104
|
}
|
|
2105
|
+
const WORKBENCH_ORDER_CACHE_KEY = 'workbenchOrderV1'
|
|
2106
|
+
function workbenchOrderScope() { return String(state.server || location.origin || 'default') }
|
|
2107
|
+
function workbenchOrderStore() {
|
|
2108
|
+
let value = null
|
|
2109
|
+
try { value = JSON.parse(LS.get(WORKBENCH_ORDER_CACHE_KEY, '{}')) } catch {}
|
|
2110
|
+
if (!value || typeof value !== 'object') value = {}
|
|
2111
|
+
if (!value.scopes || typeof value.scopes !== 'object') value.scopes = {}
|
|
2112
|
+
return value
|
|
2113
|
+
}
|
|
2114
|
+
function workbenchOrderScopeValue() {
|
|
2115
|
+
const store = workbenchOrderStore()
|
|
2116
|
+
const key = workbenchOrderScope()
|
|
2117
|
+
if (!store.scopes[key] || typeof store.scopes[key] !== 'object') store.scopes[key] = {}
|
|
2118
|
+
return { store, value: store.scopes[key] }
|
|
2119
|
+
}
|
|
2120
|
+
function orderedItems(items, ids, getId) {
|
|
2121
|
+
const source = Array.isArray(items) ? items : []
|
|
2122
|
+
const byId = new Map(source.map(item => [String(getId(item)), item]))
|
|
2123
|
+
const result = []
|
|
2124
|
+
const used = new Set()
|
|
2125
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
2126
|
+
const key = String(id)
|
|
2127
|
+
const item = byId.get(key)
|
|
2128
|
+
if (item && !used.has(key)) { result.push(item); used.add(key) }
|
|
2129
|
+
}
|
|
2130
|
+
for (const item of source) {
|
|
2131
|
+
const key = String(getId(item))
|
|
2132
|
+
if (!used.has(key)) { result.push(item); used.add(key) }
|
|
2133
|
+
}
|
|
2134
|
+
return result
|
|
2135
|
+
}
|
|
2136
|
+
function orderedWorkspaceItems(items) {
|
|
2137
|
+
const { value } = workbenchOrderScopeValue()
|
|
2138
|
+
return orderedItems(items, value.workspaceIds, item => item.workspaceId)
|
|
2139
|
+
}
|
|
2140
|
+
function orderedWorkspaceSessions(workspaceId, items) {
|
|
2141
|
+
const { value } = workbenchOrderScopeValue()
|
|
2142
|
+
return orderedItems(items, value.sessionIds?.[String(workspaceId)], item => item.sessionId)
|
|
2143
|
+
}
|
|
2144
|
+
function saveWorkbenchOrder(mutator) {
|
|
2145
|
+
const { store, value } = workbenchOrderScopeValue()
|
|
2146
|
+
mutator(value)
|
|
2147
|
+
LS.set(WORKBENCH_ORDER_CACHE_KEY, JSON.stringify(store))
|
|
2148
|
+
}
|
|
2149
|
+
function commitWorkspaceOrder(order) {
|
|
2150
|
+
saveWorkbenchOrder(value => { value.workspaceIds = order.map(String) })
|
|
2151
|
+
renderWorkbench()
|
|
2152
|
+
toast(t('ds.wbOrderSaved'), 'ok')
|
|
2153
|
+
}
|
|
2154
|
+
function commitWorkspaceSessionOrder(workspaceId, order) {
|
|
2155
|
+
if (!workspaceId) return
|
|
2156
|
+
saveWorkbenchOrder(value => {
|
|
2157
|
+
value.sessionIds ||= {}
|
|
2158
|
+
value.sessionIds[String(workspaceId)] = order.map(String)
|
|
2159
|
+
})
|
|
2160
|
+
renderWorkbench()
|
|
2161
|
+
toast(t('ds.wbOrderSaved'), 'ok')
|
|
2162
|
+
}
|
|
1951
2163
|
async function refreshWorkbench({ silent = false } = {}) {
|
|
1952
2164
|
if (!state.token) { renderWorkbench(); return }
|
|
1953
2165
|
let wb = null
|
|
@@ -1995,9 +2207,9 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
1995
2207
|
}
|
|
1996
2208
|
}
|
|
1997
2209
|
} catch {}
|
|
1998
|
-
state.wb.projects = items
|
|
2210
|
+
state.wb.projects = orderedWorkspaceItems(items
|
|
1999
2211
|
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
2000
|
-
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true }))
|
|
2212
|
+
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true })))
|
|
2001
2213
|
renderWorkbench()
|
|
2002
2214
|
renderSessions()
|
|
2003
2215
|
}
|
|
@@ -2021,27 +2233,41 @@ function renderWorkbench() {
|
|
|
2021
2233
|
const panel = $('wb-panel')
|
|
2022
2234
|
panel.classList.toggle('hidden', !state.wb.expanded)
|
|
2023
2235
|
if (!state.wb.expanded) return
|
|
2024
|
-
const projects = state.wb.projects || []
|
|
2236
|
+
const projects = orderedWorkspaceItems(state.wb.projects || [])
|
|
2025
2237
|
const archivedSet = new Set(state.archivedIds || [])
|
|
2026
2238
|
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
2027
2239
|
html += projects.length ? projects.map(w => {
|
|
2028
2240
|
const id = String(w.workspaceId || '')
|
|
2029
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
2241
|
+
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => sessionSortTime(b) - sessionSortTime(a)))
|
|
2030
2242
|
const open = state.wb.open === id
|
|
2031
|
-
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
2243
|
+
return `<div class="ds-wb-project ${open ? 'open' : ''}" data-wb-project="${esc(id)}" data-motion-key="${esc(id)}">
|
|
2032
2244
|
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
2245
|
+
<span class="ds-wb-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
|
|
2033
2246
|
<span class="ds-wb-caret" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
2034
2247
|
<span class="ds-wb-project-title" title="${esc(w.path)}">${esc(w.title || wbBaseName(w.path) || short(id))}</span>
|
|
2035
2248
|
<span class="ds-wb-project-count">${sessions.length}</span>
|
|
2036
2249
|
</button>
|
|
2037
2250
|
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
2038
2251
|
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
2039
|
-
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}"><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(titleOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
|
|
2252
|
+
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}"><span class="ds-wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(titleOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
|
|
2040
2253
|
</div>
|
|
2041
2254
|
</div>`
|
|
2042
2255
|
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
2043
2256
|
html += `<button type="button" class="ds-mini-btn ds-wb-unbind-panel" data-wb-unbind-panel>${esc(t('wb.unbind'))}</button>`
|
|
2044
|
-
|
|
2257
|
+
if (window.DshMotion?.relayout) {
|
|
2258
|
+
window.DshMotion.relayout(panel, '.ds-wb-project', () => { panel.innerHTML = html })
|
|
2259
|
+
} else panel.innerHTML = html
|
|
2260
|
+
window.DshMotion?.list(panel, '.ds-wb-session')
|
|
2261
|
+
window.DshMotion?.bindLongPressReorder(panel, '.ds-wb-project', {
|
|
2262
|
+
handleSelector: '.ds-wb-project-head',
|
|
2263
|
+
excludeSelector: '[data-wb-new]',
|
|
2264
|
+
onCommit: ({ order }) => commitWorkspaceOrder(order)
|
|
2265
|
+
})
|
|
2266
|
+
window.DshMotion?.bindLongPressReorder(panel, '.ds-wb-session', {
|
|
2267
|
+
groupSelector: '.ds-wb-project',
|
|
2268
|
+
handleSelector: '.ds-wb-session',
|
|
2269
|
+
onCommit: ({ item, order }) => commitWorkspaceSessionOrder(item.closest('[data-wb-project]')?.dataset.wbProject, order)
|
|
2270
|
+
})
|
|
2045
2271
|
panel.querySelectorAll('[data-wb-head]').forEach(button => button.addEventListener('click', () => {
|
|
2046
2272
|
state.wb.open = state.wb.open === button.dataset.wbHead ? null : button.dataset.wbHead
|
|
2047
2273
|
renderWorkbench()
|
|
@@ -2289,12 +2515,28 @@ function showView(id) {
|
|
|
2289
2515
|
state.view = id
|
|
2290
2516
|
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
2291
2517
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
2518
|
+
window.DshMotion?.view($(id))
|
|
2292
2519
|
const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
|
|
2293
2520
|
if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
|
|
2294
2521
|
else $('ds-title').textContent = t(titles[id])
|
|
2295
2522
|
if (id === 'view-overview') renderOverviewDesktop()
|
|
2296
2523
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
|
|
2297
2524
|
if (id === 'view-settings') showSettingsHome()
|
|
2525
|
+
updateSessionActions()
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function updateSessionActions() {
|
|
2529
|
+
const active = state.view === 'view-chat' && !!state.current
|
|
2530
|
+
$('btn-rename-session')?.classList.toggle('hidden', !active)
|
|
2531
|
+
$('btn-archive-session')?.classList.toggle('hidden', !active || state.archivedIds.includes(state.current))
|
|
2532
|
+
$('ds-session-status')?.classList.toggle('hidden', !active || !recoveryLabel())
|
|
2533
|
+
if (active) {
|
|
2534
|
+
const s = state.byId.get(state.current)
|
|
2535
|
+
$('ds-title').textContent = s ? titleOf(s) : t('ds.sessions')
|
|
2536
|
+
$('ds-session-status').textContent = recoveryLabel()
|
|
2537
|
+
}
|
|
2538
|
+
const running = !!state.byId.get(state.current)?.running || (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2539
|
+
$('btn-cancel')?.classList.toggle('hidden', !active || !running)
|
|
2298
2540
|
}
|
|
2299
2541
|
|
|
2300
2542
|
const SETTINGS_GROUPS = ['general', 'servers', 'theme', 'about']
|
|
@@ -2428,6 +2670,13 @@ function bindUi() {
|
|
|
2428
2670
|
$('btn-wb-path').addEventListener('click', () => { if (state.wb.path) toast(t('wb.boundPath', { path: state.wb.path }), 'ok') })
|
|
2429
2671
|
$('btn-wb-unbind').addEventListener('click', unbindWorkbench)
|
|
2430
2672
|
$('btn-send').addEventListener('click', sendMessage)
|
|
2673
|
+
$('btn-cancel').addEventListener('click', cancelSession)
|
|
2674
|
+
$('btn-rename-session').addEventListener('click', () => renameSession())
|
|
2675
|
+
$('btn-archive-session').addEventListener('click', archiveCurrentSession)
|
|
2676
|
+
$('rename-cancel').addEventListener('click', closeRenameSession)
|
|
2677
|
+
$('rename-confirm').addEventListener('click', confirmRenameSession)
|
|
2678
|
+
$('rename-session-input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) confirmRenameSession() })
|
|
2679
|
+
$('modal-rename').addEventListener('click', e => { if (e.target === $('modal-rename')) closeRenameSession() })
|
|
2431
2680
|
$('composer').addEventListener('keydown', (e) => {
|
|
2432
2681
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
2433
2682
|
})
|