dsh-remote-plugin 0.6.12 → 0.6.14
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 +353 -21
- package/index.mjs +30 -5
- package/package.json +1 -1
- package/public/admin.html +157 -8
- package/public/admin.js +264 -5
- package/public/announcements.json +52 -0
- package/public/app.js +515 -48
- package/public/desktop/desktop.css +25 -0
- package/public/desktop/desktop.html +12 -6
- package/public/desktop/desktop.js +225 -36
- package/public/index.html +61 -16
- package/public/md.js +71 -1
- package/public/styles.css +50 -0
- package/public/transcribe-core.js +74 -0
- package/public/update.json +12 -12
- 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
|
/* ---------------- 皮肤 ---------------- */
|
|
@@ -48,6 +50,9 @@ function themeSet(id) { LS.set('dshTheme', id); themeApply() }
|
|
|
48
50
|
themeApply()
|
|
49
51
|
|
|
50
52
|
/* ---------------- 状态 ---------------- */
|
|
53
|
+
function emptyDesktopHistory() {
|
|
54
|
+
return { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity, partialReasoning: new Map() }
|
|
55
|
+
}
|
|
51
56
|
const state = {
|
|
52
57
|
token: LS.get('token', ''),
|
|
53
58
|
wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
|
|
@@ -58,15 +63,19 @@ const state = {
|
|
|
58
63
|
autoSelect: { '默认': true },
|
|
59
64
|
groupActive: { '默认': '' },
|
|
60
65
|
serverLatency: {},
|
|
66
|
+
gatewayHealth: {},
|
|
61
67
|
selectingServer: false,
|
|
62
68
|
sessions: [],
|
|
63
69
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
64
70
|
byId: new Map(),
|
|
65
71
|
current: null,
|
|
66
72
|
hostInfo: null,
|
|
67
|
-
history:
|
|
73
|
+
history: emptyDesktopHistory(),
|
|
68
74
|
approvals: [],
|
|
69
75
|
questions: [],
|
|
76
|
+
queues: {},
|
|
77
|
+
queueSteering: {},
|
|
78
|
+
sessionTurnTimes: {},
|
|
70
79
|
questionModal: null,
|
|
71
80
|
streamsOk: { mux: false, host: false },
|
|
72
81
|
errCount: 0,
|
|
@@ -80,7 +89,8 @@ const state = {
|
|
|
80
89
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
81
90
|
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
82
91
|
archivedIds: [],
|
|
83
|
-
view: 'sessions'
|
|
92
|
+
view: 'sessions',
|
|
93
|
+
subagentExpandedSession: ''
|
|
84
94
|
}
|
|
85
95
|
const streams = {}
|
|
86
96
|
let pollTimer = null
|
|
@@ -280,16 +290,31 @@ function renderEffortMenu() {
|
|
|
280
290
|
const cur = state.models.current
|
|
281
291
|
const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
|
|
282
292
|
const model = (provider?.models || []).find(m => m.id === cur?.model)
|
|
283
|
-
const efforts = model
|
|
284
|
-
group.classList.toggle('hidden', !efforts.length)
|
|
293
|
+
const { efforts, defaultEffort, custom } = reasoningEffortOptions(model)
|
|
294
|
+
group.classList.toggle('hidden', !cur || !efforts.length)
|
|
285
295
|
box.innerHTML = efforts.map(e => {
|
|
286
|
-
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id ===
|
|
296
|
+
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === defaultEffort)
|
|
287
297
|
return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
|
|
288
|
-
}).join('')
|
|
298
|
+
}).join('') + (custom ? `<span class="ds-effort-hint">${esc(t('models.effortCustomHint'))}</span>` : '')
|
|
289
299
|
box.querySelectorAll('[data-effort]').forEach(btn =>
|
|
290
300
|
btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
function reasoningEffortOptions(model) {
|
|
304
|
+
const raw = Array.isArray(model?.reasoning?.efforts) && model.reasoning.efforts.length
|
|
305
|
+
? model.reasoning.efforts
|
|
306
|
+
: (Array.isArray(model?.reasoningEfforts) && model.reasoningEfforts.length ? model.reasoningEfforts : null)
|
|
307
|
+
const names = { low: t('models.effortLow'), high: t('models.effortHigh'), max: t('models.effortMax'), off: t('models.effortOff') }
|
|
308
|
+
if (raw) {
|
|
309
|
+
return {
|
|
310
|
+
efforts: raw.map(e => typeof e === 'string' ? { id: e, name: names[e] || e } : e),
|
|
311
|
+
defaultEffort: model?.reasoning?.defaultEffort,
|
|
312
|
+
custom: !model?.reasoning?.efforts
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { efforts: ['low', 'high', 'max'].map(id => ({ id, name: names[id] })), defaultEffort: undefined, custom: true }
|
|
316
|
+
}
|
|
317
|
+
|
|
293
318
|
async function selectSessionEffort(effortId) {
|
|
294
319
|
const cur = state.models.current
|
|
295
320
|
if (!state.current || !cur) return
|
|
@@ -523,9 +548,10 @@ async function getWsTicket() {
|
|
|
523
548
|
const token = state.token
|
|
524
549
|
const server = state.server
|
|
525
550
|
wsTicketPromise = (async () => {
|
|
551
|
+
if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
|
|
526
552
|
const res = await fetch(apiUrl('/api/ws-ticket'), {
|
|
527
553
|
method: 'POST',
|
|
528
|
-
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web' }
|
|
554
|
+
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
529
555
|
})
|
|
530
556
|
if (!res.ok) throw new Error('ws ticket HTTP ' + res.status)
|
|
531
557
|
const data = await res.json()
|
|
@@ -538,7 +564,7 @@ async function getWsTicket() {
|
|
|
538
564
|
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
539
565
|
const opts = {
|
|
540
566
|
method: 'POST',
|
|
541
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
567
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
542
568
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
543
569
|
}
|
|
544
570
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -555,7 +581,7 @@ async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
|
555
581
|
async function respond(rpcId, value) {
|
|
556
582
|
const opts = {
|
|
557
583
|
method: 'POST',
|
|
558
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
584
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
559
585
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
560
586
|
}
|
|
561
587
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -645,9 +671,18 @@ async function pingServer(base) {
|
|
|
645
671
|
const timer = setTimeout(() => ctrl.abort(), 3500)
|
|
646
672
|
try {
|
|
647
673
|
const res = await fetch(u + '/health?t=' + Date.now(), { signal: ctrl.signal, cache: 'no-store' })
|
|
648
|
-
|
|
674
|
+
if (!res.ok) return Infinity
|
|
675
|
+
const health = await res.json().catch(() => null)
|
|
676
|
+
if (health && typeof health === 'object') state.gatewayHealth[u] = health
|
|
677
|
+
return Math.round(performance.now() - t0)
|
|
649
678
|
} catch { return Infinity } finally { clearTimeout(timer) }
|
|
650
679
|
}
|
|
680
|
+
function activeGatewayCapability(name) {
|
|
681
|
+
const key = String(state.server || location.origin || '').replace(/\/+$/, '')
|
|
682
|
+
const capabilities = state.gatewayHealth[key]?.capabilities
|
|
683
|
+
if (!capabilities || !Object.prototype.hasOwnProperty.call(capabilities, name)) return null
|
|
684
|
+
return Number(capabilities[name]) > 0
|
|
685
|
+
}
|
|
651
686
|
async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
652
687
|
if (state.selectingServer) return null
|
|
653
688
|
state.selectingServer = true
|
|
@@ -1065,7 +1100,7 @@ async function pollKind(kind) {
|
|
|
1065
1100
|
let res
|
|
1066
1101
|
try {
|
|
1067
1102
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(5000) : undefined
|
|
1068
|
-
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1103
|
+
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1069
1104
|
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 })
|
|
1070
1105
|
} catch { return }
|
|
1071
1106
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
@@ -1135,6 +1170,7 @@ function onMuxFrame(full) {
|
|
|
1135
1170
|
return
|
|
1136
1171
|
}
|
|
1137
1172
|
if (f.type === 'question/resolved') { state.questions = state.questions.filter(q => q.rpcId !== f.questionRpcId); renderNotifStack(); return }
|
|
1173
|
+
if (f.type === 'session/queue') { state.queues[f.sessionId] = f.items || []; renderQueue(); return }
|
|
1138
1174
|
if (f.type === 'session/projection') { applyProjection(f.sessionId, f.key, f.value, f.seq); return }
|
|
1139
1175
|
if (f.type === 'stream/error') toast(f.error?.message || 'stream error', 'err')
|
|
1140
1176
|
}
|
|
@@ -1144,7 +1180,7 @@ function onHostFrame(full) {
|
|
|
1144
1180
|
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()
|
|
1145
1181
|
if (f.type === 'host/session-status') {
|
|
1146
1182
|
const s = state.byId.get(f.sessionId)
|
|
1147
|
-
if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions(); renderOverviewDesktop() }
|
|
1183
|
+
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
1148
1184
|
}
|
|
1149
1185
|
}
|
|
1150
1186
|
function applyProjection(sessionId, key, value, seq) {
|
|
@@ -1163,6 +1199,10 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1163
1199
|
}
|
|
1164
1200
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1165
1201
|
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
|
|
1202
|
+
function isTopLevelSession(session) {
|
|
1203
|
+
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1204
|
+
}
|
|
1205
|
+
function topLevelSessions() { return state.sessions.filter(isTopLevelSession) }
|
|
1166
1206
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1167
1207
|
function isGoalTerminal(goal) {
|
|
1168
1208
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -1173,10 +1213,24 @@ function goalOf(s) {
|
|
|
1173
1213
|
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1174
1214
|
}
|
|
1175
1215
|
function onSessionEvent(sessionId, event) {
|
|
1216
|
+
if (event?.type === 'turn/start' || event?.type === 'turn/end') {
|
|
1217
|
+
noteSessionTurnTime(sessionId, event)
|
|
1218
|
+
renderSessions()
|
|
1219
|
+
}
|
|
1220
|
+
const session = state.byId.get(sessionId)
|
|
1221
|
+
if (event?.type === 'agent/status' && session) {
|
|
1222
|
+
session.running = !!event.data?.running
|
|
1223
|
+
if (state.current === sessionId) { renderQueue(); updateComposerStatus() }
|
|
1224
|
+
}
|
|
1176
1225
|
if (state.current === sessionId && event) {
|
|
1177
1226
|
const h = state.history
|
|
1227
|
+
const reasoningChanged = applyReasoningStreamEvent(event)
|
|
1228
|
+
if (event.type === 'assistant/chunk' || event.type === 'reasoning-chunks') {
|
|
1229
|
+
if (reasoningChanged) scheduleReasoningRender()
|
|
1230
|
+
return
|
|
1231
|
+
}
|
|
1178
1232
|
const seq = event.seq
|
|
1179
|
-
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type)) {
|
|
1233
|
+
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type, event)) {
|
|
1180
1234
|
h.seqs.add(seq)
|
|
1181
1235
|
h.visible.push({ seq, event })
|
|
1182
1236
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -1201,6 +1255,15 @@ function sessionWorkspaceLabel(s) {
|
|
|
1201
1255
|
const cwd = sessionCwd(s)
|
|
1202
1256
|
return cwd || t('ds.workspaceUnknown')
|
|
1203
1257
|
}
|
|
1258
|
+
function sessionSortTime(s) {
|
|
1259
|
+
return Math.max(Number(state.sessionTurnTimes[s?.sessionId]) || 0, Number(s?.updatedAt) || 0, Number(s?.createdAt) || 0)
|
|
1260
|
+
}
|
|
1261
|
+
function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
1262
|
+
const raw = typeof eventOrTime === 'object' ? eventOrTime?.time : eventOrTime
|
|
1263
|
+
const time = Number(raw) > 0 ? Number(raw) : Date.now()
|
|
1264
|
+
if (!sessionId || !Number.isFinite(time)) return
|
|
1265
|
+
state.sessionTurnTimes[sessionId] = Math.max(Number(state.sessionTurnTimes[sessionId]) || 0, time)
|
|
1266
|
+
}
|
|
1204
1267
|
function workspaceDisplayName(label) {
|
|
1205
1268
|
const value = String(label || '').trim()
|
|
1206
1269
|
if (!value || value === t('ds.workspaceUnknown')) return value || t('ds.workspaceUnknown')
|
|
@@ -1209,16 +1272,16 @@ function workspaceDisplayName(label) {
|
|
|
1209
1272
|
return parts[parts.length - 1] || value
|
|
1210
1273
|
}
|
|
1211
1274
|
function sortedSessions() {
|
|
1212
|
-
const items =
|
|
1275
|
+
const items = topLevelSessions()
|
|
1213
1276
|
if (state.sessionSort === 'workspace') {
|
|
1214
1277
|
return items.sort((a, b) => {
|
|
1215
1278
|
const aw = sessionCwd(a) || '\uffff'
|
|
1216
1279
|
const bw = sessionCwd(b) || '\uffff'
|
|
1217
1280
|
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1218
|
-
return byWorkspace || ((b
|
|
1281
|
+
return byWorkspace || (sessionSortTime(b) - sessionSortTime(a))
|
|
1219
1282
|
})
|
|
1220
1283
|
}
|
|
1221
|
-
return items.sort((a, b) => (b
|
|
1284
|
+
return items.sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
1222
1285
|
}
|
|
1223
1286
|
function renderSessions() {
|
|
1224
1287
|
const allItems = sortedSessions()
|
|
@@ -1248,7 +1311,7 @@ function renderSessions() {
|
|
|
1248
1311
|
rows.push(`<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1249
1312
|
<span class="ds-session-title">${esc(title)}</span>
|
|
1250
1313
|
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1251
|
-
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s
|
|
1314
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(sessionSortTime(s))}</span>
|
|
1252
1315
|
</button>`)
|
|
1253
1316
|
}
|
|
1254
1317
|
return rows.join('')
|
|
@@ -1272,20 +1335,24 @@ function renderSessions() {
|
|
|
1272
1335
|
|
|
1273
1336
|
async function openSession(id) {
|
|
1274
1337
|
state.current = id
|
|
1275
|
-
state.history =
|
|
1338
|
+
state.history = emptyDesktopHistory()
|
|
1276
1339
|
state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
1277
1340
|
showView('view-chat')
|
|
1278
1341
|
$('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
|
|
1279
1342
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
1280
1343
|
renderSessions()
|
|
1281
1344
|
renderSessionCards()
|
|
1345
|
+
renderQueue()
|
|
1346
|
+
updateComposerStatus()
|
|
1282
1347
|
await loadHistory()
|
|
1283
1348
|
}
|
|
1284
1349
|
function closeSession() {
|
|
1285
1350
|
state.current = null
|
|
1286
|
-
state.history =
|
|
1351
|
+
state.history = emptyDesktopHistory()
|
|
1287
1352
|
const cards = $('session-cards')
|
|
1288
1353
|
if (cards) cards.innerHTML = ''
|
|
1354
|
+
renderQueue()
|
|
1355
|
+
updateComposerStatus()
|
|
1289
1356
|
showView('view-sessions')
|
|
1290
1357
|
}
|
|
1291
1358
|
async function loadHistory() {
|
|
@@ -1303,8 +1370,10 @@ async function loadHistory() {
|
|
|
1303
1370
|
for (const entry of v.events || []) {
|
|
1304
1371
|
const ev = entry?.event
|
|
1305
1372
|
const seq = ev?.seq
|
|
1373
|
+
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
1374
|
+
applyReasoningStreamEvent(ev)
|
|
1306
1375
|
if (seq == null || state.history.seqs.has(seq)) continue
|
|
1307
|
-
if (!shouldShowEvent(ev.type)) continue
|
|
1376
|
+
if (!shouldShowEvent(ev.type, ev)) continue
|
|
1308
1377
|
state.history.seqs.add(seq)
|
|
1309
1378
|
state.history.visible.push({ seq, event: ev })
|
|
1310
1379
|
}
|
|
@@ -1321,12 +1390,78 @@ const INTERESTING_EVENTS = new Set([
|
|
|
1321
1390
|
'todo/updated', 'plan/updated', 'question/asked', 'question/resolved',
|
|
1322
1391
|
'approval/asked', 'approval/resolved', 'session/title', 'title'
|
|
1323
1392
|
])
|
|
1324
|
-
function
|
|
1393
|
+
function messageSource(data) {
|
|
1394
|
+
const source = data?.source ?? data?.message?.source
|
|
1395
|
+
return source && typeof source === 'object' ? source : null
|
|
1396
|
+
}
|
|
1397
|
+
function isHumanUserMessage(event) {
|
|
1398
|
+
if (event?.type !== 'user/message') return false
|
|
1399
|
+
const source = messageSource(event.data || {})
|
|
1400
|
+
// Older DSH events may not carry source metadata; keep those visible for compatibility.
|
|
1401
|
+
return !source || source.kind === 'user'
|
|
1402
|
+
}
|
|
1403
|
+
function shouldShowEvent(type, event) {
|
|
1404
|
+
if (!INTERESTING_EVENTS.has(type)) return false
|
|
1405
|
+
if (type === 'user/message' && !isHumanUserMessage(event)) {
|
|
1406
|
+
const data = event?.data || {}
|
|
1407
|
+
const blocks = data.message?.content || data.content || []
|
|
1408
|
+
return systemReminderText(blocks).length > 0
|
|
1409
|
+
}
|
|
1410
|
+
return true
|
|
1411
|
+
}
|
|
1412
|
+
function reasoningStreamKey(data, index) { return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}` }
|
|
1413
|
+
function applyReasoningStreamEvent(event) {
|
|
1414
|
+
const h = state.history
|
|
1415
|
+
const data = event?.data || {}
|
|
1416
|
+
let changed = false
|
|
1417
|
+
if (event?.type === 'assistant/chunk') {
|
|
1418
|
+
const chunk = data.chunk || {}
|
|
1419
|
+
const key = reasoningStreamKey(data, chunk.index)
|
|
1420
|
+
if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') {
|
|
1421
|
+
h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: '' })
|
|
1422
|
+
changed = true
|
|
1423
|
+
} else if (chunk.type === 'reasoning-delta') {
|
|
1424
|
+
const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: chunk.index, text: '' }
|
|
1425
|
+
item.text += String(chunk.text || '')
|
|
1426
|
+
h.partialReasoning.set(key, item)
|
|
1427
|
+
changed = true
|
|
1428
|
+
} else if (chunk.type === 'block-end' && chunk.block?.type === 'reasoning') {
|
|
1429
|
+
h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: String(chunk.block.text ?? chunk.block.content ?? '') })
|
|
1430
|
+
changed = true
|
|
1431
|
+
}
|
|
1432
|
+
} else if (event?.type === 'reasoning-chunks') {
|
|
1433
|
+
const key = reasoningStreamKey(data, data.index)
|
|
1434
|
+
const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: data.index, text: '' }
|
|
1435
|
+
item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
|
|
1436
|
+
h.partialReasoning.set(key, item)
|
|
1437
|
+
changed = true
|
|
1438
|
+
} else if (event?.type === 'assistant/message') {
|
|
1439
|
+
for (const [key, item] of h.partialReasoning) {
|
|
1440
|
+
if (item.turn === data.turn && item.step === data.step) { h.partialReasoning.delete(key); changed = true }
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return changed
|
|
1444
|
+
}
|
|
1445
|
+
let reasoningRenderTimer = null
|
|
1446
|
+
function scheduleReasoningRender() {
|
|
1447
|
+
if (reasoningRenderTimer) return
|
|
1448
|
+
reasoningRenderTimer = setTimeout(() => {
|
|
1449
|
+
reasoningRenderTimer = null
|
|
1450
|
+
if (state.current) renderHistory()
|
|
1451
|
+
}, 80)
|
|
1452
|
+
}
|
|
1453
|
+
function partialReasoningHtml() {
|
|
1454
|
+
return [...state.history.partialReasoning.values()]
|
|
1455
|
+
.filter(item => item.text)
|
|
1456
|
+
.sort((a, b) => (a.turn ?? 0) - (b.turn ?? 0) || (a.step ?? 0) - (b.step ?? 0) || (a.index ?? 0) - (b.index ?? 0))
|
|
1457
|
+
.map(item => `<div class="ds-msg assistant ds-reasoning-live"><div class="role">${esc(t('ds.role.dsh'))}</div><details open><summary>${esc(t('block.thinkingLive'))}</summary><div>${esc(item.text)}</div></details></div>`)
|
|
1458
|
+
.join('')
|
|
1459
|
+
}
|
|
1325
1460
|
function safeJson(v) { try { return JSON.stringify(v, null, 2) } catch { return String(v) } }
|
|
1326
1461
|
function blockHtml(b) {
|
|
1327
1462
|
if (!b) return ''
|
|
1328
1463
|
if (b.type === 'text') return `<div class="md">${window.mdToHtml ? window.mdToHtml(b.text ?? '') : esc(b.text ?? '')}</div>`
|
|
1329
|
-
if (b.type === 'reasoning') return `<
|
|
1464
|
+
if (b.type === 'thinking' || b.type === 'reasoning') return `<details><summary>${esc(t('block.thinking'))}</summary><div style="opacity:.82">${esc(b.text ?? b.content ?? '')}</div></details>`
|
|
1330
1465
|
if (b.type === 'tool-call') return `<div>🔧 ${esc(b.name || '')}</div>`
|
|
1331
1466
|
if (b.type === 'tool-result') return `<div>📦</div>`
|
|
1332
1467
|
if (b.type === 'image') return `<div>🖼</div>`
|
|
@@ -1343,7 +1478,7 @@ function eventHtml(entry) {
|
|
|
1343
1478
|
const ev = entry.event || {}
|
|
1344
1479
|
const data = ev.data || {}
|
|
1345
1480
|
const type = ev.type || 'event'
|
|
1346
|
-
if (!shouldShowEvent(type)) return ''
|
|
1481
|
+
if (!shouldShowEvent(type, ev)) return ''
|
|
1347
1482
|
if (type === 'user/message' || type === 'assistant/message') {
|
|
1348
1483
|
const msg = data.message || {}
|
|
1349
1484
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
@@ -1353,6 +1488,7 @@ function eventHtml(entry) {
|
|
|
1353
1488
|
const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
|
|
1354
1489
|
return `<details class="event ds-tool ds-event-detail"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1355
1490
|
}
|
|
1491
|
+
if (type === 'user/message' && !isHumanUserMessage(ev)) return ''
|
|
1356
1492
|
const text = blocks.map(blockHtml).join('')
|
|
1357
1493
|
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>`
|
|
1358
1494
|
}
|
|
@@ -1373,7 +1509,8 @@ function eventHtml(entry) {
|
|
|
1373
1509
|
function renderHistory() {
|
|
1374
1510
|
const box = $('history')
|
|
1375
1511
|
const items = state.history.visible
|
|
1376
|
-
|
|
1512
|
+
const html = items.map(eventHtml).join('') + partialReasoningHtml()
|
|
1513
|
+
box.innerHTML = html || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
|
|
1377
1514
|
box.scrollTop = box.scrollHeight
|
|
1378
1515
|
}
|
|
1379
1516
|
|
|
@@ -1412,13 +1549,19 @@ async function renderSessionCards() {
|
|
|
1412
1549
|
const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
|
|
1413
1550
|
if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
|
|
1414
1551
|
if (sub?.entries?.length) {
|
|
1552
|
+
const expanded = state.subagentExpandedSession === sessionId
|
|
1553
|
+
const toggleLabel = expanded ? t('subagent.collapse') : t('subagent.expand')
|
|
1415
1554
|
const rows = sub.entries.map(e => {
|
|
1416
1555
|
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>`
|
|
1417
1556
|
const label = e.label || short(e.id)
|
|
1418
1557
|
const running = e.activity === 'running'
|
|
1419
1558
|
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>`
|
|
1420
1559
|
}).join('')
|
|
1421
|
-
box.insertAdjacentHTML('beforeend', `<div class="ds-card"><
|
|
1560
|
+
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">${expanded ? '⌃' : '⌄'}</span></button><div class="ds-subagent-list${expanded ? '' : ' hidden'}">${rows}</div></div>`)
|
|
1561
|
+
box.querySelector('[data-subagent-toggle]')?.addEventListener('click', () => {
|
|
1562
|
+
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
1563
|
+
renderSessionCards()
|
|
1564
|
+
})
|
|
1422
1565
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1423
1566
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
1424
1567
|
}
|
|
@@ -1481,7 +1624,7 @@ async function runSlashCommand(text) {
|
|
|
1481
1624
|
: undefined
|
|
1482
1625
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1483
1626
|
method: 'POST',
|
|
1484
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
1627
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
1485
1628
|
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
1486
1629
|
...(signal ? { signal } : {})
|
|
1487
1630
|
})
|
|
@@ -1507,7 +1650,52 @@ async function sendMessage() {
|
|
|
1507
1650
|
mode: 'queue',
|
|
1508
1651
|
content: [{ type: 'text', text }]
|
|
1509
1652
|
}, '')
|
|
1510
|
-
if (v) toast(t('ds.toastSent'), 'ok')
|
|
1653
|
+
if (v) { noteSessionTurnTime(state.current, Date.now()); renderSessions(); toast(t('ds.toastSent'), 'ok') }
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function updateComposerStatus() {
|
|
1657
|
+
const status = $('composer-status')
|
|
1658
|
+
if (!status) return
|
|
1659
|
+
status.classList.toggle('hidden', !state.byId.get(state.current)?.running)
|
|
1660
|
+
}
|
|
1661
|
+
function queuePreview(item) {
|
|
1662
|
+
const blocks = item?.message?.content || item?.content || []
|
|
1663
|
+
const text = Array.isArray(blocks)
|
|
1664
|
+
? blocks.filter(block => block?.type === 'text').map(block => String(block.text || '')).join(' ').trim()
|
|
1665
|
+
: ''
|
|
1666
|
+
return text || (Array.isArray(blocks) && blocks.some(block => block?.type === 'image') ? t('queue.image') : '…')
|
|
1667
|
+
}
|
|
1668
|
+
async function steerQueueItem(itemId) {
|
|
1669
|
+
const sessionId = state.current
|
|
1670
|
+
const key = `${sessionId}:${itemId}`
|
|
1671
|
+
const s = state.byId.get(sessionId)
|
|
1672
|
+
if (!sessionId || !s?.running || state.queueSteering[key]) return
|
|
1673
|
+
state.queueSteering[key] = true
|
|
1674
|
+
renderQueue()
|
|
1675
|
+
try {
|
|
1676
|
+
const v = await safeRpc('session.updateQueue', { sessionId, itemId, action: { kind: 'steer' } }, t('queue.steerFailed', { msg: '' }).replace(/:$/, '').replace(/: $/, ''))
|
|
1677
|
+
if (v?.accepted) toast(t('queue.steerSubmitted'), 'ok')
|
|
1678
|
+
} finally {
|
|
1679
|
+
delete state.queueSteering[key]
|
|
1680
|
+
renderQueue()
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
function renderQueue() {
|
|
1684
|
+
const box = $('queue-dock')
|
|
1685
|
+
if (!box) return
|
|
1686
|
+
const sessionId = state.current
|
|
1687
|
+
const s = state.byId.get(sessionId)
|
|
1688
|
+
const items = (state.queues[sessionId] || []).filter(item => item?.placement === 'queued')
|
|
1689
|
+
box.classList.toggle('hidden', !items.length)
|
|
1690
|
+
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 => {
|
|
1691
|
+
const key = `${sessionId}:${item.id}`
|
|
1692
|
+
const busy = !!state.queueSteering[key]
|
|
1693
|
+
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>`
|
|
1694
|
+
}).join('')}</div>` : ''
|
|
1695
|
+
box.querySelectorAll('[data-queue-steer]').forEach(button => {
|
|
1696
|
+
button.addEventListener('click', () => steerQueueItem(button.dataset.queueSteer))
|
|
1697
|
+
})
|
|
1698
|
+
updateComposerStatus()
|
|
1511
1699
|
}
|
|
1512
1700
|
|
|
1513
1701
|
/* ---------------- 审批/提问通知卡片栈 ---------------- */
|
|
@@ -1621,7 +1809,7 @@ function fsApiUrl(sub, params = {}) {
|
|
|
1621
1809
|
return u.href
|
|
1622
1810
|
}
|
|
1623
1811
|
function fsHeaders() {
|
|
1624
|
-
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1812
|
+
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1625
1813
|
}
|
|
1626
1814
|
function fsParent(p) {
|
|
1627
1815
|
if (!p) return null
|
|
@@ -1749,7 +1937,7 @@ function wbFsParent(p) {
|
|
|
1749
1937
|
return raw.slice(0, index)
|
|
1750
1938
|
}
|
|
1751
1939
|
async function wbGateway(method, pathname, body) {
|
|
1752
|
-
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
1940
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } }
|
|
1753
1941
|
if (body !== undefined) {
|
|
1754
1942
|
options.headers['content-type'] = 'application/json'
|
|
1755
1943
|
options.body = JSON.stringify(body)
|
|
@@ -1838,7 +2026,7 @@ function renderWorkbench() {
|
|
|
1838
2026
|
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1839
2027
|
html += projects.length ? projects.map(w => {
|
|
1840
2028
|
const id = String(w.workspaceId || '')
|
|
1841
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(
|
|
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))
|
|
1842
2030
|
const open = state.wb.open === id
|
|
1843
2031
|
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
1844
2032
|
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
@@ -1964,7 +2152,7 @@ async function loadStats() {
|
|
|
1964
2152
|
return
|
|
1965
2153
|
}
|
|
1966
2154
|
try {
|
|
1967
|
-
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
2155
|
+
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } })
|
|
1968
2156
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1969
2157
|
const json = await res.json()
|
|
1970
2158
|
renderStats(json.days || [])
|
|
@@ -2060,8 +2248,9 @@ function renderOverviewDesktop() {
|
|
|
2060
2248
|
$('ds-overview-attention-list').querySelectorAll('[data-ds-overview-approve]').forEach(btn => btn.addEventListener('click', () => approveApproval(btn.closest('[data-ds-overview-approval]')?.dataset.dsOverviewApproval || '', btn.dataset.dsOverviewApprove === '1')))
|
|
2061
2249
|
$('ds-overview-attention-list').querySelectorAll('[data-ds-overview-question]').forEach(btn => btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.dsOverviewQuestion))))
|
|
2062
2250
|
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
2251
|
+
const topSessions = topLevelSessions()
|
|
2252
|
+
const running = topSessions.filter(s => s.running).length
|
|
2253
|
+
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (sessionSortTime(b) - sessionSortTime(a))).slice(0, 6)
|
|
2065
2254
|
const primary = $('ds-overview-primary-action')
|
|
2066
2255
|
if (primary) {
|
|
2067
2256
|
let action = 'new'
|
|
@@ -2091,7 +2280,7 @@ function renderOverviewDesktop() {
|
|
|
2091
2280
|
$('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
|
|
2092
2281
|
$('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
|
|
2093
2282
|
$('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)}">
|
|
2094
|
-
<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
|
|
2283
|
+
<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>
|
|
2095
2284
|
</button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
|
|
2096
2285
|
$('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
|
|
2097
2286
|
}
|