dsh-remote-plugin 0.6.13 → 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 +261 -27
- package/package.json +1 -1
- package/public/announcements.json +25 -0
- package/public/app.js +570 -62
- package/public/desktop/desktop.css +42 -7
- package/public/desktop/desktop.html +30 -6
- package/public/desktop/desktop.js +395 -46
- package/public/index.html +57 -20
- package/public/md.js +71 -1
- 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 +47 -6
- package/public/transcribe-core.js +74 -0
- 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
|
@@ -13,14 +13,16 @@ const LS = {
|
|
|
13
13
|
}
|
|
14
14
|
const CLIENT_ID = (() => {
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
const key = 'dshRemoteClientIdV2'
|
|
17
|
+
let id = localStorage.getItem(key)
|
|
17
18
|
if (!id) {
|
|
18
19
|
id = (globalThis.crypto?.randomUUID?.() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`)
|
|
19
|
-
|
|
20
|
+
localStorage.setItem(key, id)
|
|
20
21
|
}
|
|
21
22
|
return id
|
|
22
23
|
} catch { return '' }
|
|
23
24
|
})()
|
|
25
|
+
function clientIdHeaders() { return CLIENT_ID ? { 'x-dsh-remote-client-id': CLIENT_ID } : {} }
|
|
24
26
|
const CAP = window.Capacitor || null
|
|
25
27
|
|
|
26
28
|
/* ---------------- 皮肤 ---------------- */
|
|
@@ -67,10 +69,16 @@ const state = {
|
|
|
67
69
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
68
70
|
byId: new Map(),
|
|
69
71
|
current: null,
|
|
72
|
+
sessionRecovery: { status: 'idle', error: '' },
|
|
73
|
+
pendingProjections: new Map(),
|
|
74
|
+
lastStreamResyncAt: 0,
|
|
70
75
|
hostInfo: null,
|
|
71
76
|
history: emptyDesktopHistory(),
|
|
72
77
|
approvals: [],
|
|
73
78
|
questions: [],
|
|
79
|
+
queues: {},
|
|
80
|
+
queueSteering: {},
|
|
81
|
+
sessionTurnTimes: {},
|
|
74
82
|
questionModal: null,
|
|
75
83
|
streamsOk: { mux: false, host: false },
|
|
76
84
|
errCount: 0,
|
|
@@ -84,7 +92,8 @@ const state = {
|
|
|
84
92
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
85
93
|
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
86
94
|
archivedIds: [],
|
|
87
|
-
view: 'sessions'
|
|
95
|
+
view: 'sessions',
|
|
96
|
+
subagentExpandedSession: ''
|
|
88
97
|
}
|
|
89
98
|
const streams = {}
|
|
90
99
|
let pollTimer = null
|
|
@@ -545,7 +554,7 @@ async function getWsTicket() {
|
|
|
545
554
|
if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
|
|
546
555
|
const res = await fetch(apiUrl('/api/ws-ticket'), {
|
|
547
556
|
method: 'POST',
|
|
548
|
-
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web' }
|
|
557
|
+
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
549
558
|
})
|
|
550
559
|
if (!res.ok) throw new Error('ws ticket HTTP ' + res.status)
|
|
551
560
|
const data = await res.json()
|
|
@@ -558,7 +567,7 @@ async function getWsTicket() {
|
|
|
558
567
|
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
559
568
|
const opts = {
|
|
560
569
|
method: 'POST',
|
|
561
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
570
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
562
571
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
563
572
|
}
|
|
564
573
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -575,7 +584,7 @@ async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
|
575
584
|
async function respond(rpcId, value) {
|
|
576
585
|
const opts = {
|
|
577
586
|
method: 'POST',
|
|
578
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
587
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
579
588
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
580
589
|
}
|
|
581
590
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -1011,6 +1020,7 @@ function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
|
1011
1020
|
updateConn()
|
|
1012
1021
|
if (kind === 'mux') { state.approvals = []; state.questions = []; renderNotifStack() }
|
|
1013
1022
|
if (refreshOnOpen) refreshSessions()
|
|
1023
|
+
if (allStreamsOpen()) resyncAfterStreamOpen()
|
|
1014
1024
|
}
|
|
1015
1025
|
ws.onmessage = (msg) => {
|
|
1016
1026
|
if (!streamIsCurrent(kind, ws, generation)) return
|
|
@@ -1094,7 +1104,7 @@ async function pollKind(kind) {
|
|
|
1094
1104
|
let res
|
|
1095
1105
|
try {
|
|
1096
1106
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(5000) : undefined
|
|
1097
|
-
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1107
|
+
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1098
1108
|
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 })
|
|
1099
1109
|
} catch { return }
|
|
1100
1110
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
@@ -1107,6 +1117,7 @@ async function pollKind(kind) {
|
|
|
1107
1117
|
state.pollSeq[kind] = 0
|
|
1108
1118
|
if (kind === 'mux') renderNotifStack()
|
|
1109
1119
|
refreshSessions()
|
|
1120
|
+
if (state.current) void resyncCurrentSession()
|
|
1110
1121
|
}
|
|
1111
1122
|
for (const item of data.events) {
|
|
1112
1123
|
if (item.seq > (state.pollSeq[kind] || 0)) {
|
|
@@ -1164,6 +1175,7 @@ function onMuxFrame(full) {
|
|
|
1164
1175
|
return
|
|
1165
1176
|
}
|
|
1166
1177
|
if (f.type === 'question/resolved') { state.questions = state.questions.filter(q => q.rpcId !== f.questionRpcId); renderNotifStack(); return }
|
|
1178
|
+
if (f.type === 'session/queue') { state.queues[f.sessionId] = f.items || []; renderQueue(); return }
|
|
1167
1179
|
if (f.type === 'session/projection') { applyProjection(f.sessionId, f.key, f.value, f.seq); return }
|
|
1168
1180
|
if (f.type === 'stream/error') toast(f.error?.message || 'stream error', 'err')
|
|
1169
1181
|
}
|
|
@@ -1173,23 +1185,70 @@ function onHostFrame(full) {
|
|
|
1173
1185
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) refreshSessions()
|
|
1174
1186
|
if (f.type === 'host/session-status') {
|
|
1175
1187
|
const s = state.byId.get(f.sessionId)
|
|
1176
|
-
if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions(); renderOverviewDesktop() }
|
|
1188
|
+
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
1189
|
+
}
|
|
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)
|
|
1177
1208
|
}
|
|
1178
1209
|
}
|
|
1179
1210
|
function applyProjection(sessionId, key, value, seq) {
|
|
1180
1211
|
const s = state.byId.get(sessionId)
|
|
1181
|
-
if (s) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
|
1186
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)
|
|
1187
1225
|
if (state.current === sessionId) {
|
|
1188
1226
|
renderSessions()
|
|
1189
1227
|
if (['goal', 'todos'].includes(key)) renderSessionCards()
|
|
1190
1228
|
}
|
|
1191
1229
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) refreshSessions()
|
|
1192
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
|
+
}
|
|
1193
1252
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1194
1253
|
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
|
|
1195
1254
|
function isTopLevelSession(session) {
|
|
@@ -1206,6 +1265,15 @@ function goalOf(s) {
|
|
|
1206
1265
|
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1207
1266
|
}
|
|
1208
1267
|
function onSessionEvent(sessionId, event) {
|
|
1268
|
+
if (event?.type === 'turn/start' || event?.type === 'turn/end') {
|
|
1269
|
+
noteSessionTurnTime(sessionId, event)
|
|
1270
|
+
renderSessions()
|
|
1271
|
+
}
|
|
1272
|
+
const session = state.byId.get(sessionId)
|
|
1273
|
+
if (event?.type === 'agent/status' && session) {
|
|
1274
|
+
session.running = !!event.data?.running
|
|
1275
|
+
if (state.current === sessionId) { renderQueue(); updateComposerStatus() }
|
|
1276
|
+
}
|
|
1209
1277
|
if (state.current === sessionId && event) {
|
|
1210
1278
|
const h = state.history
|
|
1211
1279
|
const reasoningChanged = applyReasoningStreamEvent(event)
|
|
@@ -1214,7 +1282,7 @@ function onSessionEvent(sessionId, event) {
|
|
|
1214
1282
|
return
|
|
1215
1283
|
}
|
|
1216
1284
|
const seq = event.seq
|
|
1217
|
-
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type)) {
|
|
1285
|
+
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type, event)) {
|
|
1218
1286
|
h.seqs.add(seq)
|
|
1219
1287
|
h.visible.push({ seq, event })
|
|
1220
1288
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -1230,6 +1298,7 @@ async function refreshSessions() {
|
|
|
1230
1298
|
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1231
1299
|
state.sessions = v.items || []
|
|
1232
1300
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1301
|
+
applyPendingProjections()
|
|
1233
1302
|
renderSessions()
|
|
1234
1303
|
scheduleWorkbenchRefresh()
|
|
1235
1304
|
renderOverviewDesktop()
|
|
@@ -1239,6 +1308,23 @@ function sessionWorkspaceLabel(s) {
|
|
|
1239
1308
|
const cwd = sessionCwd(s)
|
|
1240
1309
|
return cwd || t('ds.workspaceUnknown')
|
|
1241
1310
|
}
|
|
1311
|
+
function sessionSortTime(s) {
|
|
1312
|
+
return Math.max(Number(state.sessionTurnTimes[s?.sessionId]) || 0, Number(s?.updatedAt) || 0, Number(s?.createdAt) || 0)
|
|
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
|
+
}
|
|
1322
|
+
function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
1323
|
+
const raw = typeof eventOrTime === 'object' ? eventOrTime?.time : eventOrTime
|
|
1324
|
+
const time = Number(raw) > 0 ? Number(raw) : Date.now()
|
|
1325
|
+
if (!sessionId || !Number.isFinite(time)) return
|
|
1326
|
+
state.sessionTurnTimes[sessionId] = Math.max(Number(state.sessionTurnTimes[sessionId]) || 0, time)
|
|
1327
|
+
}
|
|
1242
1328
|
function workspaceDisplayName(label) {
|
|
1243
1329
|
const value = String(label || '').trim()
|
|
1244
1330
|
if (!value || value === t('ds.workspaceUnknown')) return value || t('ds.workspaceUnknown')
|
|
@@ -1253,10 +1339,10 @@ function sortedSessions() {
|
|
|
1253
1339
|
const aw = sessionCwd(a) || '\uffff'
|
|
1254
1340
|
const bw = sessionCwd(b) || '\uffff'
|
|
1255
1341
|
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1256
|
-
return byWorkspace || ((b
|
|
1342
|
+
return byWorkspace || (sessionSortTime(b) - sessionSortTime(a))
|
|
1257
1343
|
})
|
|
1258
1344
|
}
|
|
1259
|
-
return items.sort((a, b) => (b
|
|
1345
|
+
return items.sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
1260
1346
|
}
|
|
1261
1347
|
function renderSessions() {
|
|
1262
1348
|
const allItems = sortedSessions()
|
|
@@ -1272,30 +1358,54 @@ function renderSessions() {
|
|
|
1272
1358
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1273
1359
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1274
1360
|
const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
|
|
1275
|
-
const
|
|
1276
|
-
let lastWorkspace = null
|
|
1277
|
-
const rows = []
|
|
1278
|
-
for (const s of items) {
|
|
1361
|
+
const renderSession = s => {
|
|
1279
1362
|
const workspace = sessionWorkspaceLabel(s)
|
|
1280
1363
|
const workspaceName = workspaceDisplayName(workspace)
|
|
1281
|
-
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1282
|
-
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>`)
|
|
1283
|
-
lastWorkspace = workspace
|
|
1284
|
-
}
|
|
1285
1364
|
const title = titleOf(s)
|
|
1286
|
-
|
|
1365
|
+
return `<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1287
1366
|
<span class="ds-session-title">${esc(title)}</span>
|
|
1288
1367
|
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1289
|
-
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s
|
|
1290
|
-
</button>`
|
|
1368
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(sessionSortTime(s))}</span>
|
|
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)
|
|
1291
1384
|
}
|
|
1292
|
-
|
|
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('')
|
|
1293
1390
|
}
|
|
1294
1391
|
const divider = archived.length ? `<button class="ds-archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1295
1392
|
const hiddenByWorkbench = allItems.length - visible.length
|
|
1296
1393
|
const html = renderItems(main) + divider + (showArchived ? renderItems(archived) : '') || `<div class="ds-empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('ds.sessionsEmpty'))}</div>`
|
|
1297
1394
|
$('session-list').innerHTML = html
|
|
1298
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
|
+
}
|
|
1299
1409
|
$('session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1300
1410
|
$('mobile-session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1301
1411
|
const sort = $('session-sort')
|
|
@@ -1310,46 +1420,60 @@ function renderSessions() {
|
|
|
1310
1420
|
|
|
1311
1421
|
async function openSession(id) {
|
|
1312
1422
|
state.current = id
|
|
1423
|
+
setSessionRecovery('loading')
|
|
1313
1424
|
state.history = emptyDesktopHistory()
|
|
1314
1425
|
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
1315
1426
|
showView('view-chat')
|
|
1316
1427
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
1428
|
+
updateSessionActions()
|
|
1317
1429
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
1318
1430
|
renderSessions()
|
|
1319
1431
|
renderSessionCards()
|
|
1432
|
+
renderQueue()
|
|
1433
|
+
updateComposerStatus()
|
|
1320
1434
|
await loadHistory()
|
|
1321
1435
|
}
|
|
1322
1436
|
function closeSession() {
|
|
1323
1437
|
state.current = null
|
|
1438
|
+
setSessionRecovery('idle')
|
|
1324
1439
|
state.history = emptyDesktopHistory()
|
|
1325
1440
|
const cards = $('session-cards')
|
|
1326
1441
|
if (cards) cards.innerHTML = ''
|
|
1442
|
+
renderQueue()
|
|
1443
|
+
updateComposerStatus()
|
|
1444
|
+
updateSessionActions()
|
|
1327
1445
|
showView('view-sessions')
|
|
1328
1446
|
}
|
|
1329
1447
|
async function loadHistory() {
|
|
1330
1448
|
const id = state.current
|
|
1331
1449
|
if (!id || state.history.loading) return
|
|
1332
1450
|
state.history.loading = true
|
|
1451
|
+
setSessionRecovery('loading')
|
|
1333
1452
|
let v
|
|
1334
1453
|
try { v = await rpc('session.history', { sessionId: id, maxMessages: 60 }) }
|
|
1335
1454
|
catch (e) {
|
|
1336
1455
|
state.history.loading = false
|
|
1337
1456
|
if (e.message === 'AUTH') return
|
|
1457
|
+
setSessionRecovery('error', e.message)
|
|
1338
1458
|
$('history').innerHTML = `<div class="ds-empty">${e.message}</div>`
|
|
1339
1459
|
return
|
|
1340
1460
|
}
|
|
1461
|
+
hydrateSessionProjections(id, v.projections)
|
|
1341
1462
|
for (const entry of v.events || []) {
|
|
1342
1463
|
const ev = entry?.event
|
|
1343
1464
|
const seq = ev?.seq
|
|
1465
|
+
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
1344
1466
|
applyReasoningStreamEvent(ev)
|
|
1345
1467
|
if (seq == null || state.history.seqs.has(seq)) continue
|
|
1346
|
-
if (!shouldShowEvent(ev.type)) continue
|
|
1468
|
+
if (!shouldShowEvent(ev.type, ev)) continue
|
|
1347
1469
|
state.history.seqs.add(seq)
|
|
1348
1470
|
state.history.visible.push({ seq, event: ev })
|
|
1349
1471
|
}
|
|
1350
1472
|
state.history.visible.sort((a, b) => a.seq - b.seq)
|
|
1351
1473
|
state.history.hasMore = !!v.hasMore
|
|
1352
1474
|
state.history.loading = false
|
|
1475
|
+
setSessionRecovery('ready')
|
|
1476
|
+
updateSessionActions()
|
|
1353
1477
|
renderHistory()
|
|
1354
1478
|
}
|
|
1355
1479
|
|
|
@@ -1360,7 +1484,25 @@ const INTERESTING_EVENTS = new Set([
|
|
|
1360
1484
|
'todo/updated', 'plan/updated', 'question/asked', 'question/resolved',
|
|
1361
1485
|
'approval/asked', 'approval/resolved', 'session/title', 'title'
|
|
1362
1486
|
])
|
|
1363
|
-
function
|
|
1487
|
+
function messageSource(data) {
|
|
1488
|
+
const source = data?.source ?? data?.message?.source
|
|
1489
|
+
return source && typeof source === 'object' ? source : null
|
|
1490
|
+
}
|
|
1491
|
+
function isHumanUserMessage(event) {
|
|
1492
|
+
if (event?.type !== 'user/message') return false
|
|
1493
|
+
const source = messageSource(event.data || {})
|
|
1494
|
+
// Older DSH events may not carry source metadata; keep those visible for compatibility.
|
|
1495
|
+
return !source || source.kind === 'user'
|
|
1496
|
+
}
|
|
1497
|
+
function shouldShowEvent(type, event) {
|
|
1498
|
+
if (!INTERESTING_EVENTS.has(type)) return false
|
|
1499
|
+
if (type === 'user/message' && !isHumanUserMessage(event)) {
|
|
1500
|
+
const data = event?.data || {}
|
|
1501
|
+
const blocks = data.message?.content || data.content || []
|
|
1502
|
+
return systemReminderText(blocks).length > 0
|
|
1503
|
+
}
|
|
1504
|
+
return true
|
|
1505
|
+
}
|
|
1364
1506
|
function reasoningStreamKey(data, index) { return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}` }
|
|
1365
1507
|
function applyReasoningStreamEvent(event) {
|
|
1366
1508
|
const h = state.history
|
|
@@ -1430,7 +1572,7 @@ function eventHtml(entry) {
|
|
|
1430
1572
|
const ev = entry.event || {}
|
|
1431
1573
|
const data = ev.data || {}
|
|
1432
1574
|
const type = ev.type || 'event'
|
|
1433
|
-
if (!shouldShowEvent(type)) return ''
|
|
1575
|
+
if (!shouldShowEvent(type, ev)) return ''
|
|
1434
1576
|
if (type === 'user/message' || type === 'assistant/message') {
|
|
1435
1577
|
const msg = data.message || {}
|
|
1436
1578
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
@@ -1440,6 +1582,7 @@ function eventHtml(entry) {
|
|
|
1440
1582
|
const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
|
|
1441
1583
|
return `<details class="event ds-tool ds-event-detail"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1442
1584
|
}
|
|
1585
|
+
if (type === 'user/message' && !isHumanUserMessage(ev)) return ''
|
|
1443
1586
|
const text = blocks.map(blockHtml).join('')
|
|
1444
1587
|
return `<div class="ds-msg ${esc(role)}"><div class="role">${esc(role === 'user' ? t('ds.role.me') : t('ds.role.dsh'))}</div>${text || '<span style="opacity:.6">…</span>'}</div>`
|
|
1445
1588
|
}
|
|
@@ -1500,13 +1643,24 @@ async function renderSessionCards() {
|
|
|
1500
1643
|
const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
|
|
1501
1644
|
if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
|
|
1502
1645
|
if (sub?.entries?.length) {
|
|
1646
|
+
const expanded = state.subagentExpandedSession === sessionId
|
|
1647
|
+
const toggleLabel = expanded ? t('subagent.collapse') : t('subagent.expand')
|
|
1503
1648
|
const rows = sub.entries.map(e => {
|
|
1504
1649
|
if (e.kind === 'diagnostic') return `<div class="ds-card-row"><span class="ds-card-k">${t('subagent.diagnostic')}</span><span class="ds-card-v">${esc(e.reason)}</span></div>`
|
|
1505
1650
|
const label = e.label || short(e.id)
|
|
1506
1651
|
const running = e.activity === 'running'
|
|
1507
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>`
|
|
1508
1653
|
}).join('')
|
|
1509
|
-
|
|
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>`)
|
|
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')
|
|
1661
|
+
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
1662
|
+
setTimeout(() => renderSessionCards(), 240)
|
|
1663
|
+
})
|
|
1510
1664
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1511
1665
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
1512
1666
|
}
|
|
@@ -1569,7 +1723,7 @@ async function runSlashCommand(text) {
|
|
|
1569
1723
|
: undefined
|
|
1570
1724
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1571
1725
|
method: 'POST',
|
|
1572
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
1726
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
1573
1727
|
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
1574
1728
|
...(signal ? { signal } : {})
|
|
1575
1729
|
})
|
|
@@ -1590,12 +1744,112 @@ async function sendMessage() {
|
|
|
1590
1744
|
if (!text || !state.current) return
|
|
1591
1745
|
if (await runSlashCommand(text)) { input.value = ''; return }
|
|
1592
1746
|
input.value = ''
|
|
1747
|
+
setSessionRecovery('resuming')
|
|
1593
1748
|
const v = await safeRpc('session.prompt', {
|
|
1594
1749
|
sessionId: state.current,
|
|
1595
1750
|
mode: 'queue',
|
|
1596
1751
|
content: [{ type: 'text', text }]
|
|
1597
1752
|
}, '')
|
|
1598
|
-
if (v) 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()
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function updateComposerStatus() {
|
|
1810
|
+
const status = $('composer-status')
|
|
1811
|
+
if (!status) return
|
|
1812
|
+
status.classList.toggle('hidden', !state.byId.get(state.current)?.running)
|
|
1813
|
+
updateSessionActions()
|
|
1814
|
+
}
|
|
1815
|
+
function queuePreview(item) {
|
|
1816
|
+
const blocks = item?.message?.content || item?.content || []
|
|
1817
|
+
const text = Array.isArray(blocks)
|
|
1818
|
+
? blocks.filter(block => block?.type === 'text').map(block => String(block.text || '')).join(' ').trim()
|
|
1819
|
+
: ''
|
|
1820
|
+
return text || (Array.isArray(blocks) && blocks.some(block => block?.type === 'image') ? t('queue.image') : '…')
|
|
1821
|
+
}
|
|
1822
|
+
async function steerQueueItem(itemId) {
|
|
1823
|
+
const sessionId = state.current
|
|
1824
|
+
const key = `${sessionId}:${itemId}`
|
|
1825
|
+
const s = state.byId.get(sessionId)
|
|
1826
|
+
if (!sessionId || !s?.running || state.queueSteering[key]) return
|
|
1827
|
+
state.queueSteering[key] = true
|
|
1828
|
+
renderQueue()
|
|
1829
|
+
try {
|
|
1830
|
+
const v = await safeRpc('session.updateQueue', { sessionId, itemId, action: { kind: 'steer' } }, t('queue.steerFailed', { msg: '' }).replace(/:$/, '').replace(/: $/, ''))
|
|
1831
|
+
if (v?.accepted) toast(t('queue.steerSubmitted'), 'ok')
|
|
1832
|
+
} finally {
|
|
1833
|
+
delete state.queueSteering[key]
|
|
1834
|
+
renderQueue()
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function renderQueue() {
|
|
1838
|
+
const box = $('queue-dock')
|
|
1839
|
+
if (!box) return
|
|
1840
|
+
const sessionId = state.current
|
|
1841
|
+
const s = state.byId.get(sessionId)
|
|
1842
|
+
const items = (state.queues[sessionId] || []).filter(item => item?.placement === 'queued')
|
|
1843
|
+
box.classList.toggle('hidden', !items.length)
|
|
1844
|
+
box.innerHTML = items.length ? `<div class="ds-queue-dock-head"><span>⌁</span><span>${esc(t('queue.title'))} · ${items.length}</span></div><div class="ds-queue-dock-list">${items.map(item => {
|
|
1845
|
+
const key = `${sessionId}:${item.id}`
|
|
1846
|
+
const busy = !!state.queueSteering[key]
|
|
1847
|
+
return `<div class="ds-queue-dock-item"><span class="ds-queue-dock-preview" title="${esc(queuePreview(item))}">${esc(queuePreview(item))}</span><button type="button" class="ds-mini-btn ds-queue-dock-action" data-queue-steer="${esc(item.id)}" title="${esc(s?.running ? t('queue.steer') : t('queue.steerUnavailable'))}" ${s?.running && !busy ? '' : 'disabled'}>${busy ? '…' : esc(t('queue.steer'))}</button></div>`
|
|
1848
|
+
}).join('')}</div>` : ''
|
|
1849
|
+
box.querySelectorAll('[data-queue-steer]').forEach(button => {
|
|
1850
|
+
button.addEventListener('click', () => steerQueueItem(button.dataset.queueSteer))
|
|
1851
|
+
})
|
|
1852
|
+
updateComposerStatus()
|
|
1599
1853
|
}
|
|
1600
1854
|
|
|
1601
1855
|
/* ---------------- 审批/提问通知卡片栈 ---------------- */
|
|
@@ -1709,7 +1963,7 @@ function fsApiUrl(sub, params = {}) {
|
|
|
1709
1963
|
return u.href
|
|
1710
1964
|
}
|
|
1711
1965
|
function fsHeaders() {
|
|
1712
|
-
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1966
|
+
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1713
1967
|
}
|
|
1714
1968
|
function fsParent(p) {
|
|
1715
1969
|
if (!p) return null
|
|
@@ -1837,7 +2091,7 @@ function wbFsParent(p) {
|
|
|
1837
2091
|
return raw.slice(0, index)
|
|
1838
2092
|
}
|
|
1839
2093
|
async function wbGateway(method, pathname, body) {
|
|
1840
|
-
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
2094
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } }
|
|
1841
2095
|
if (body !== undefined) {
|
|
1842
2096
|
options.headers['content-type'] = 'application/json'
|
|
1843
2097
|
options.body = JSON.stringify(body)
|
|
@@ -1848,6 +2102,64 @@ async function wbGateway(method, pathname, body) {
|
|
|
1848
2102
|
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1849
2103
|
return data
|
|
1850
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
|
+
}
|
|
1851
2163
|
async function refreshWorkbench({ silent = false } = {}) {
|
|
1852
2164
|
if (!state.token) { renderWorkbench(); return }
|
|
1853
2165
|
let wb = null
|
|
@@ -1895,9 +2207,9 @@ async function refreshWorkbench({ silent = false } = {}) {
|
|
|
1895
2207
|
}
|
|
1896
2208
|
}
|
|
1897
2209
|
} catch {}
|
|
1898
|
-
state.wb.projects = items
|
|
2210
|
+
state.wb.projects = orderedWorkspaceItems(items
|
|
1899
2211
|
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
1900
|
-
.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 })))
|
|
1901
2213
|
renderWorkbench()
|
|
1902
2214
|
renderSessions()
|
|
1903
2215
|
}
|
|
@@ -1921,27 +2233,41 @@ function renderWorkbench() {
|
|
|
1921
2233
|
const panel = $('wb-panel')
|
|
1922
2234
|
panel.classList.toggle('hidden', !state.wb.expanded)
|
|
1923
2235
|
if (!state.wb.expanded) return
|
|
1924
|
-
const projects = state.wb.projects || []
|
|
2236
|
+
const projects = orderedWorkspaceItems(state.wb.projects || [])
|
|
1925
2237
|
const archivedSet = new Set(state.archivedIds || [])
|
|
1926
2238
|
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1927
2239
|
html += projects.length ? projects.map(w => {
|
|
1928
2240
|
const id = String(w.workspaceId || '')
|
|
1929
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b
|
|
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)))
|
|
1930
2242
|
const open = state.wb.open === id
|
|
1931
|
-
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)}">
|
|
1932
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>
|
|
1933
2246
|
<span class="ds-wb-caret" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1934
2247
|
<span class="ds-wb-project-title" title="${esc(w.path)}">${esc(w.title || wbBaseName(w.path) || short(id))}</span>
|
|
1935
2248
|
<span class="ds-wb-project-count">${sessions.length}</span>
|
|
1936
2249
|
</button>
|
|
1937
2250
|
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
1938
2251
|
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
1939
|
-
${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>`}
|
|
1940
2253
|
</div>
|
|
1941
2254
|
</div>`
|
|
1942
2255
|
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1943
2256
|
html += `<button type="button" class="ds-mini-btn ds-wb-unbind-panel" data-wb-unbind-panel>${esc(t('wb.unbind'))}</button>`
|
|
1944
|
-
|
|
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
|
+
})
|
|
1945
2271
|
panel.querySelectorAll('[data-wb-head]').forEach(button => button.addEventListener('click', () => {
|
|
1946
2272
|
state.wb.open = state.wb.open === button.dataset.wbHead ? null : button.dataset.wbHead
|
|
1947
2273
|
renderWorkbench()
|
|
@@ -2052,7 +2378,7 @@ async function loadStats() {
|
|
|
2052
2378
|
return
|
|
2053
2379
|
}
|
|
2054
2380
|
try {
|
|
2055
|
-
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
2381
|
+
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } })
|
|
2056
2382
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2057
2383
|
const json = await res.json()
|
|
2058
2384
|
renderStats(json.days || [])
|
|
@@ -2150,7 +2476,7 @@ function renderOverviewDesktop() {
|
|
|
2150
2476
|
|
|
2151
2477
|
const topSessions = topLevelSessions()
|
|
2152
2478
|
const running = topSessions.filter(s => s.running).length
|
|
2153
|
-
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (
|
|
2479
|
+
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (sessionSortTime(b) - sessionSortTime(a))).slice(0, 6)
|
|
2154
2480
|
const primary = $('ds-overview-primary-action')
|
|
2155
2481
|
if (primary) {
|
|
2156
2482
|
let action = 'new'
|
|
@@ -2180,7 +2506,7 @@ function renderOverviewDesktop() {
|
|
|
2180
2506
|
$('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
|
|
2181
2507
|
$('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
|
|
2182
2508
|
$('ds-overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="ds-overview-session-item ${s.running ? 'running' : ''}" data-ds-overview-session="${esc(s.sessionId)}">
|
|
2183
|
-
<span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(titleOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(s
|
|
2509
|
+
<span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(titleOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="ds-overview-arrow">›</span>
|
|
2184
2510
|
</button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
|
|
2185
2511
|
$('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
|
|
2186
2512
|
}
|
|
@@ -2189,12 +2515,28 @@ function showView(id) {
|
|
|
2189
2515
|
state.view = id
|
|
2190
2516
|
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
2191
2517
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
2518
|
+
window.DshMotion?.view($(id))
|
|
2192
2519
|
const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
|
|
2193
2520
|
if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
|
|
2194
2521
|
else $('ds-title').textContent = t(titles[id])
|
|
2195
2522
|
if (id === 'view-overview') renderOverviewDesktop()
|
|
2196
2523
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
|
|
2197
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)
|
|
2198
2540
|
}
|
|
2199
2541
|
|
|
2200
2542
|
const SETTINGS_GROUPS = ['general', 'servers', 'theme', 'about']
|
|
@@ -2328,6 +2670,13 @@ function bindUi() {
|
|
|
2328
2670
|
$('btn-wb-path').addEventListener('click', () => { if (state.wb.path) toast(t('wb.boundPath', { path: state.wb.path }), 'ok') })
|
|
2329
2671
|
$('btn-wb-unbind').addEventListener('click', unbindWorkbench)
|
|
2330
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() })
|
|
2331
2680
|
$('composer').addEventListener('keydown', (e) => {
|
|
2332
2681
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
2333
2682
|
})
|