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
|
@@ -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
|
})
|
package/public/index.html
CHANGED
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
<div id="session-title" class="session-title" data-i18n="session.loading">加载中…</div>
|
|
94
94
|
<div id="session-sub" class="session-sub"></div>
|
|
95
95
|
</div>
|
|
96
|
+
<button id="btn-rename-session" class="icon-btn hidden" data-i18n-title="session.rename" data-i18n-aria="session.rename">✎</button>
|
|
97
|
+
<button id="btn-archive-session" class="mini-btn hidden" data-i18n="session.archive">归档</button>
|
|
96
98
|
<button id="btn-stats" class="icon-btn" data-i18n-title="stats.title" data-i18n-aria="stats.title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19V9M12 19V5M19 19v-7M3 19h18"/></svg></button>
|
|
97
99
|
<button id="btn-fs-send" class="session-send-btn hidden" type="button" data-i18n="composer.send" data-i18n-title="composer.send" data-i18n-aria="composer.send">发送</button>
|
|
98
100
|
<button id="btn-cancel" class="danger-btn hidden" data-i18n="session.stop">停止</button>
|
|
@@ -158,8 +160,7 @@
|
|
|
158
160
|
<div id="composer-input-wrap" class="composer-input-wrap">
|
|
159
161
|
<textarea id="composer-input" rows="1" data-i18n-placeholder="composer.placeholder" placeholder="给 DSH 发消息…"></textarea>
|
|
160
162
|
<button id="btn-fs-toggle" class="composer-fs-btn hidden" type="button" data-i18n-title="composer.fullscreen" data-i18n-aria="composer.fullscreen">
|
|
161
|
-
<
|
|
162
|
-
<svg id="fs-ico-collapse" class="hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M9 3v6H3M15 3v6h6M9 21v-6H3M21 15h-6v6"/></svg>
|
|
163
|
+
<morph-icon id="fs-ico" data-morph-state="closed" data-morph-closed="M8 3H3v5M16 3h5v5M8 21H3v-5M21 16v5h-5" data-morph-open="M9 3v6H3M15 3v6h6M9 21v-6H3M21 15h-6v6" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M21 16v5h-5"/></svg></morph-icon>
|
|
163
164
|
</button>
|
|
164
165
|
</div>
|
|
165
166
|
<button id="btn-image" class="composer-image-btn" type="button" data-i18n-title="composer.addImage" data-i18n-aria="composer.addImage"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="4" width="17" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.2"/><path d="m5 17 4.5-4.5 3 3 2-2L19 17"/></svg></button>
|
|
@@ -653,6 +654,19 @@
|
|
|
653
654
|
</div>
|
|
654
655
|
</div>
|
|
655
656
|
|
|
657
|
+
<div id="modal-rename" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="rename-session-title">
|
|
658
|
+
<div class="modal-card">
|
|
659
|
+
<div id="rename-session-title" class="modal-title" data-i18n="session.renameTitle">重命名会话</div>
|
|
660
|
+
<div class="modal-body">
|
|
661
|
+
<input id="rename-session-input" class="text-input" maxlength="160" autocomplete="off" data-i18n-placeholder="session.renamePlaceholder" placeholder="输入新的会话名称">
|
|
662
|
+
</div>
|
|
663
|
+
<div class="modal-actions">
|
|
664
|
+
<button id="rename-cancel" class="btn subtle" data-i18n="session.renameCancel">取消</button>
|
|
665
|
+
<button id="rename-confirm" class="btn primary" data-i18n="session.renameConfirm">保存</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
656
670
|
<!-- goal 模态 -->
|
|
657
671
|
<div id="modal-goal" class="modal hidden" role="dialog" aria-modal="true">
|
|
658
672
|
<div class="modal-card">
|
|
@@ -812,7 +826,7 @@
|
|
|
812
826
|
'statsPage.gatewayDown': '统计需要网关运行', 'statsPage.empty': '暂无统计,产生会话后自动聚合',
|
|
813
827
|
'statsPage.note': '注:本数据仅在使用 DeepSeek 官方 API 时估算;基于 token 计算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
814
828
|
'home.newSession': '+ 新会话', 'home.newWorkspace': '+ 工作区', 'home.empty': '暂无会话', 'home.createFailed': '新建会话失败', 'home.created': '会话已创建',
|
|
815
|
-
'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
|
|
829
|
+
'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.orderSaved': '顺序已保存到当前设备', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
|
|
816
830
|
'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.exists': '该目录已存在', 'workspace.invalidName': '名称不能包含路径分隔符', 'workspace.createFailed': '创建工作区失败:{msg}', 'workspace.created': '工作区已创建', 'workspace.createdNoSession': '工作区已创建,但新会话未能打开',
|
|
817
831
|
'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.created': '工作区已创建', 'workspace.sessionFailed': '目录已创建,但新会话打开失败:{msg}',
|
|
818
832
|
'workspace.current': '当前工作区', 'workspace.select': '选择工作区', 'workspace.all': '全部工作区', 'workspace.ungrouped': '未分组会话', 'workspace.files': '查看文件', 'workspace.allPath': '显示全部 DSH 工作区中的会话',
|
|
@@ -849,8 +863,8 @@
|
|
|
849
863
|
'groups.confirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'groups.deleted': '组已删除',
|
|
850
864
|
'stream.error': '事件流错误:{msg}',
|
|
851
865
|
'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
|
|
852
|
-
'session.stop': '
|
|
853
|
-
'session.confirmStop': '
|
|
866
|
+
'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
|
|
867
|
+
'session.confirmStop': '停止当前回合?排队中的消息不会被删除。', 'session.stopFailed': '停止失败', 'session.stopRequested': '已请求停止本轮', 'session.archive': '归档', 'session.archiveConfirm': '归档这个会话?归档后可在“显示已归档会话”中打开。', 'session.archiveFailed': '归档失败:{msg}', 'session.archived': '会话已归档', 'session.archiveTitle': '确认归档会话', 'session.archiveDesc': '归档后会话将从普通列表隐藏,但不会删除对话记录。', 'session.archiveConversation': '对话', 'session.archiveWorkspace': '工作区', 'session.archiveCancel': '取消', 'session.archiveConfirmAction': '确认归档', 'session.rename': '重命名会话', 'session.renameTitle': '重命名会话', 'session.renamePlaceholder': '输入新的会话名称', 'session.renameCancel': '取消', 'session.renameConfirm': '保存', 'session.renameEmpty': '会话名称不能为空', 'session.renameFailed': '重命名失败', 'session.renamed': '会话名称已更新',
|
|
854
868
|
'notify.approvalTitle': '工具审批', 'notify.approvalBody': '{tool} 需要批准', 'notify.questionTitle': 'DSH 提问', 'notify.questionBody': '需要你回答',
|
|
855
869
|
'notify.permissionFailed': '通知权限申请失败:{msg}',
|
|
856
870
|
'history.loading': '加载历史…', 'history.cacheFallback': '网络不可用:显示本地缓存的历史', 'history.loadFailed': '加载历史失败:{msg}', 'history.retry': '重试',
|
|
@@ -1024,7 +1038,7 @@
|
|
|
1024
1038
|
'statsPage.gatewayDown': 'Stats require the gateway', 'statsPage.empty': 'No stats yet — they aggregate as sessions happen',
|
|
1025
1039
|
'statsPage.note': 'Note: estimates assume the official DeepSeek API. Token-based calculation may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
|
|
1026
1040
|
'home.newSession': '+ New session', 'home.newWorkspace': '+ Workspace', 'home.empty': 'No sessions yet', 'home.createFailed': 'Failed to create session', 'home.created': 'Session created',
|
|
1027
|
-
'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
|
|
1041
|
+
'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.orderSaved': 'Order saved on this device', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
|
|
1028
1042
|
'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.exists': 'That folder already exists', 'workspace.invalidName': 'The name cannot contain path separators', 'workspace.createFailed': 'Could not create workspace: {msg}', 'workspace.created': 'Workspace created', 'workspace.createdNoSession': 'Workspace created, but the new session could not be opened',
|
|
1029
1043
|
'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.created': 'Workspace created', 'workspace.sessionFailed': 'Folder created, but the new session could not be opened: {msg}',
|
|
1030
1044
|
'workspace.current': 'Current workspace', 'workspace.select': 'Select workspace', 'workspace.all': 'All workspaces', 'workspace.ungrouped': 'Ungrouped sessions', 'workspace.files': 'View files', 'workspace.allPath': 'Showing sessions from every DSH workspace',
|
|
@@ -1061,8 +1075,8 @@
|
|
|
1061
1075
|
'groups.confirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'groups.deleted': 'Group deleted',
|
|
1062
1076
|
'stream.error': 'Event stream error: {msg}',
|
|
1063
1077
|
'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
|
|
1064
|
-
'session.stop': 'Stop', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}',
|
|
1065
|
-
'session.confirmStop': 'Stop the
|
|
1078
|
+
'session.stop': 'Stop turn', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}', 'session.recovering': 'Restoring session…', 'session.recoveryReady': 'Session ready', 'session.recoveryFailed': 'Session restore failed', 'session.recoveryCached': 'Viewing cached history',
|
|
1079
|
+
'session.confirmStop': 'Stop the current turn? Queued messages will be kept.', 'session.stopFailed': 'Stop failed', 'session.stopRequested': 'Stop requested', 'session.archive': 'Archive', 'session.archiveConfirm': 'Archive this session? You can open it from “Show archived”.', 'session.archiveFailed': 'Archive failed: {msg}', 'session.archived': 'Session archived', 'session.archiveTitle': 'Confirm archive', 'session.archiveDesc': 'The session will be hidden from the normal list, but its conversation will not be deleted.', 'session.archiveConversation': 'Conversation', 'session.archiveWorkspace': 'Workspace', 'session.archiveCancel': 'Cancel', 'session.archiveConfirmAction': 'Archive session', 'session.rename': 'Rename session', 'session.renameTitle': 'Rename session', 'session.renamePlaceholder': 'Enter a new session name', 'session.renameCancel': 'Cancel', 'session.renameConfirm': 'Save', 'session.renameEmpty': 'Session name cannot be empty', 'session.renameFailed': 'Rename failed', 'session.renamed': 'Session name updated',
|
|
1066
1080
|
'notify.approvalTitle': 'Tool approval', 'notify.approvalBody': '{tool} needs approval', 'notify.questionTitle': 'DSH question', 'notify.questionBody': 'Needs your answer',
|
|
1067
1081
|
'notify.permissionFailed': 'Notification permission failed: {msg}',
|
|
1068
1082
|
'history.loading': 'Loading history…', 'history.cacheFallback': 'Network unavailable: showing cached history', 'history.loadFailed': 'Failed to load history: {msg}', 'history.retry': 'Retry',
|
|
@@ -1219,6 +1233,9 @@
|
|
|
1219
1233
|
<script src="i18n.js"></script>
|
|
1220
1234
|
<script src="theme.js"></script>
|
|
1221
1235
|
<script src="md.js"></script>
|
|
1236
|
+
<script src="vendor/gsap/gsap.min.js"></script>
|
|
1237
|
+
<script src="motion.js"></script>
|
|
1238
|
+
<script type="module" src="morphicons-init.js"></script>
|
|
1222
1239
|
<script src="app.js"></script>
|
|
1223
1240
|
</body>
|
|
1224
1241
|
</html>
|